Aug 13, 2022Kuina-chan


This is the tutorial 4 to learn the basic features of Kuin Programming Language for exe execution environment. This time, let's create an action game with a horizontal perspective.

1Prepare Images

1.1Create A Window



This time I will be creating a game using images. The completed screen is shown in Figure 1-1.
Completed Screen
Figure 1-1: Completed Screen
First, as before, insert the code for "main And A draw Control" from the snippet (Figure 1-2).
  1. var wndMain: wnd@Wnd
  2. var drawMain: wnd@Draw
  3.  
  4. func main()
  5.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  6.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  7.  
  8.   while(wnd@act())
  9.     do draw@render(60)
  10.   end while
  11. end func
Figure 1-2: kui_action1.kn
In this state, let's first save the source code in a folder somewhere. Create an empty folder and save the file in it as "main.kn" or any other name you like.
Kuin source code names can use lowercase letters, numbers, and the "_" symbol, and have a ".kn" extension. However, numbers cannot be used as the first character.

1.2Prepare Images



Now let's prepare the images for the game.
First, click 'Open "res" Folder' in Figure 1-3 to open the res folder. A folder named "res" will be created in the location of the .kn file you just saved, and it should now be open.
Open res Folder
Figure 1-3: Open res Folder
In Kuin, the files to be loaded in the program are basically placed in this res folder. If you put them here, the res folder will be encrypted and combined into a single file at release build time.
Now, you can draw your own images to load, but this time let's use the free images that come with Kuin. Copy the four files dot_back_side.png, dot_kuina_chan.png, dot_map_chips_side.png, and map_sample_side.txt from samples/free_resources/ into the res folder (Figure 1-4).
Prepare Images
Figure 1-4: Prepare Images
You can now call the images from the program.

2Show Background

Let's load and display the background image. Add the program shown in Figure 2-1.
  1. var wndMain: wnd@Wnd
  2. var drawMain: wnd@Draw
  3. var texBack: draw@Tex
  4.  
  5. func main()
  6.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  7.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  8.   do @texBack :: draw@makeTex("res/dot_back_side.png")
  9.   do draw@sampler(%point)
  10.  
  11.   while(wnd@act())
  12.     do @texBack.drawScale(0.0, 0.0, 1600.0, 900.0, 0.0, 0.0, 800.0, 450.0, draw@white)
  13.     do draw@render(60)
  14.   end while
  15. end func
Figure 2-1: kui_action2.kn
The 3rd line, draw@Tex, is a class that can store images. In line 8, the draw@makeTex function reads the res/dot_back_side.png image file that was placed earlier and assigns it to the @texBack variable. The argument in parentheses of the draw@makeTex function is the file path of the image to load.
In line 12, the @texBack.drawScale function draws the image stored in @texBack. This .drawScale is a function in the draw@Tex class. .drawScale is a function to draw a stretched image, where the arguments in parentheses are the destination's coordinate X, coordinate Y, width, and height, and the source's coordinate X, coordinate Y, width, height, and color, respectively (Figure 2-2).
drawScale Function
Figure 2-2: drawScale Function
The draw@white specified as a color means white, which is the same as writing "0xFFFFFFFF". If you specify white, the image will be drawn as is; if you make it closer to black, the image will be drawn darker.
The draw@sampler function in line 9 sets the interpolation method when the image is stretched. If %point is specified, no interpolation will be done, and if %linear is specified, interpolation will be done and the appearance will be smoother. In this case, I used %point, which is without interpolation, because I want the dots to look clearer.
When run, it will look like Figure 2-3.
Draw Background
Figure 2-3: Draw Background

3Show Character

Before we start displaying the charactor, let's create a class to handle the coordinates of the character.
This time, I want to realize the physical behavior when colliding with the map, so I will inherit and use the useful class game@Rect provided in Kuin. Inheritance is the process of creating a new class by adding variables and functions to an existing class (Figure 3-1).
  1. class Player(game@Rect)
  2.   +var toRight: bool
  3. end class
  4.  
  5. var wndMain: wnd@Wnd
  6. var drawMain: wnd@Draw
  7. var texBack: draw@Tex
  8. var texPlayer: draw@Tex
  9. var player: @Player
  10.  
  11. func main()
  12.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  13.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  14.   do @texBack :: draw@makeTex("res/dot_back_side.png")
  15.   do @texPlayer :: draw@makeTex("res/dot_kuina_chan.png")
  16.   do @player :: #@Player
  17.   do @player.x :: 96.0
  18.   do @player.y :: 800.0
  19.   do @player.width :: 46.0
  20.   do @player.height :: 116.0
  21.   do @player.toRight :: true
  22.   do draw@sampler(%point)
  23.  
  24.   while(wnd@act())
  25.     do @texBack.drawScale(0.0, 0.0, 1600.0, 900.0, 0.0, 0.0, 800.0, 450.0, draw@white)
  26.     do @texPlayer.drawScale(@player.x - 64.0, @player.y - 64.0, 128.0, 128.0, 0.0, 0.0, 64.0, 64.0, draw@white)
  27.     do draw@render(60)
  28.   end while
  29. end func
Figure 3-1: kui_action3.kn
Lines 1-3 create the @Player class. I have written game@Rect in parentheses. By writing the name of an existing class in parentheses like this, you can create a class that inherits the contents of that class. The game@Rect class has variables to hold the x and y coordinates.
The toRight variable that I add to the class in the 2nd line is of type bool, and the bool type is a type that stores true or false. In the program, true is written as "true" and false is written as "false". The toRight variable will contain whether the character is facing right or not, and will be set to true if the character is facing right and false if the character is facing left.
In lines 8-9, I have created a variable @texPlayer to contain the image for the player, and a variable @player of the @Player class type that I created earlier, and set the contents of each in lines 15-21.
In line 26, the character is drawn at the player's coordinates @player.x and @player.y. I want to draw the player so that the player's coordinates are exactly at the center of the image, so I subtract 64.0, which is half the width and height of the character (Figure 3-2).
Image Center
Figure 3-2: Image Center
When run, it will look like Figure 3-3.
Drawing Player
Figure 3-3: Drawing Player

4Move Character

Add character movement processing (Figure 4-1).
  1. class Player(game@Rect)
  2.   +var toRight: bool
  3. end class
  4.  
  5. var wndMain: wnd@Wnd
  6. var drawMain: wnd@Draw
  7. var texBack: draw@Tex
  8. var texPlayer: draw@Tex
  9. var player: @Player
  10.  
  11. func main()
  12.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  13.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  14.   do @texBack :: draw@makeTex("res/dot_back_side.png")
  15.   do @texPlayer :: draw@makeTex("res/dot_kuina_chan.png")
  16.   do @player :: #@Player
  17.   do @player.x :: 96.0
  18.   do @player.y :: 800.0
  19.   do @player.width :: 46.0
  20.   do @player.height :: 116.0
  21.   do @player.toRight :: true
  22.   do draw@sampler(%point)
  23.   var pattern: []float :: [0.0, 64.0, 0.0, 128.0]
  24.  
  25.   while(wnd@act())
  26.     do @texBack.drawScale(0.0, 0.0, 1600.0, 900.0, 0.0, 0.0, 800.0, 450.0, draw@white)
  27.     if(input@pad(0, %left) >= 1)
  28.       do @player.veloX :- 0.8
  29.       do @player.toRight :: false
  30.     end if
  31.     if(input@pad(0, %right) >= 1)
  32.       do @player.veloX :+ 0.8
  33.       do @player.toRight :: true
  34.     end if
  35.     do @player.move(0.0)
  36.     do @texPlayer.drawScale(@player.x - 64.0, @player.y - 64.0, 128.0, 128.0, pattern[draw@cnt() / 12 % 4], @player.toRight ?(128.0, 0.0), 64.0, 64.0, draw@white)
  37.     do draw@render(60)
  38.   end while
  39. end func
Figure 4-1: kui_action4.kn
In lines 27 to 34, when the right or left key is pressed, the @player.veloX variable, which represents the player's velocity, is increased or decreased, and the direction the player is facing is also set.
Line 35, "@player.move(0.0)" is a function that adds velocity to the player's position and moves it. If you put a value greater than 0.0 in parentheses, the velocity will be automatically adjusted so that it does not exceed that value. However, in this case, I want to set the maximum velocity separately for the vertical and horizontal directions, so I gave it 0.0 so that it will not be adjusted automatically.
The character is animated by rewriting line 36. The "@player.toRight ?(128.0, 0.0)" will be set to 128.0 if the player is facing right, and 0.0 if the player is facing left. The "?(,)" operator is a conditional branching operator like an if statement. If you write "A ?(B, C)", it will return B if A is true, and C if A is false.
The "pattern[...]" part in line 36 accesses the array variable created in line 23. An array is like a series of variables. For example, by setting the variable type to []float as shown in line 23, multiple float values can be stored. When reading and writing array values, write "variable name[element index]".
I have assigned "[0.0, 64.0, 0.0, 128.0]" to the pattern variable in line 23. In this case, if I write pattern[0], I get a value of 0.0; if I write pattern[1], I get a value of 64.0; if I write pattern[2], I get a value of 0.0; if I write pattern[3], I get a value of 128.0.
The variable in line 23 is created in the function, unlike the other variables. In this way, it becomes a variable that is created when the function is called and destroyed when the process exits the function. Such variables inside a function are called local variables, and variables outside the function are called global variables. When accessing global variables, you need to prefix the variable name with "@".
Line 36, "draw@cnt()" is a function that returns the number of frames that have passed since the program was started. Combining the fact that the decimal point is truncated when the "/" operator for division is applied to an integer type, and the "%" operator for determining the remainder of the division, "draw@cnt() / 12 % 4" produces a repeating pattern of "0, 1, 2, 3, 0, 1, 2, 3, 0, ...". This is how the walking animation is rendered.
When run, it will look like Figure 4-2.
Moving The Player
Figure 4-2: Moving The Player

5Draw Map

The next step is to draw the map. Add the program shown in Figure 5-1.
  1. class Player(game@Rect)
  2.   +var toRight: bool
  3. end class
  4.  
  5. var wndMain: wnd@Wnd
  6. var drawMain: wnd@Draw
  7. var texBack: draw@Tex
  8. var texPlayer: draw@Tex
  9. var player: @Player
  10. var texMap: draw@Tex
  11. var map: game@Map
  12.  
  13. func main()
  14.   do @wndMain :: wnd@makeWnd(null, %aspect, 1600, 900, "Title")
  15.   do @drawMain :: wnd@makeDraw(@wndMain, 0, 0, 1600, 900, %scale, %scale, false)
  16.   do @texBack :: draw@makeTex("res/dot_back_side.png")
  17.   do @texPlayer :: draw@makeTex("res/dot_kuina_chan.png")
  18.   do @texMap :: draw@makeTex("res/dot_map_chips_side.png")
  19.   do @map :: game@makeMap("res/map_sample_side.txt", 64.0, 64.0)
  20.   do @player :: #@Player
  21.   do @player.x :: 96.0
  22.   do @player.y :: 800.0
  23.   do @player.width :: 46.0
  24.   do @player.height :: 116.0
  25.   do @player.toRight :: true
  26.   do draw@sampler(%point)
  27.   var pattern: []float :: [0.0, 64.0, 0.0, 128.0]
  28.  
  29.   while(wnd@act())
  30.     do @texBack.drawScale(0.0, 0.0, 1600.0, 900.0, 0.0, 0.0, 800.0, 450.0, draw@white)
  31.     if(input@pad(0, %left) >= 1)
  32.       do @player.veloX :- 0.8
  33.       do @player.toRight :: false
  34.     end if
  35.     if(input@pad(0, %right) >= 1)
  36.       do @player.veloX :+ 0.8
  37.       do @player.toRight :: true
  38.     end if
  39.     do @player.move(0.0)
  40.     do @texPlayer.drawScale(@player.x - 64.0, @player.y - 64.0, 128.0, 128.0, pattern[draw@cnt() / 12 % 4], @player.toRight ?(128.0, 0.0), 64.0, 64.0, draw@white)
  41.     for y(0, 14)
  42.       for x(0, 25)
  43.         var chip: int :: @map.get(x, y)
  44.         if(chip >= 0)
  45.           do @texMap.drawScale(x $ float * 64.0 - 32.0, y $ float * 64.0 - 32.0, 64.0, 64.0, chip $ float * 32.0, 0.0, 32.0, 32.0, draw@white)
  46.         end if
  47.       end for
  48.     end for
  49.     do draw@render(60)
  50.   end while
  51. end func
Figure 5-1: kui_action5.kn
In lines 10-11, I have created a variable to hold the texture and data for the map, which is read and stored in lines 18-19. The game@Map class in line 11 has the features to easily handle a 2D map.
Line 19, game@makeMap is a function that reads map data written in a specific format. The arguments in its parentheses are, in order, map data file path, map chip width, and map chip height. The width and height of the map chip are used to determine the collision between the map and the character.
Lines 41-48 draw the map. Using the for statement explained previously, the process is repeated while counting up from 0 to 14 in lines 41 and 48, "for y(0, 14) ... end for". This for statement contains y. In this way, y can be accessed in the for statement as if it were a variable, and we can get the value during the counting process.
Similarly, in lines 42 and 47, "for x(0, 25) ... end for", the process is repeated counting up x from 0 to 25. Thus, in this case, the value of y is initially 0 and the value of x is "0, 1, 2, ..., 25," then y is 1 and x is "0, ..., 25," y is 2 and x is "0, ..., 25," and so on until y is 14 and x is 25.
Lines 43-46 draw each map chip for these x and y, and then draw the entire map. In line 43, the map chip at the (x,y) coordinates is obtained from the map data in @map and placed in the variable chip. This int type is a type that holds an integer. When the chip is less than 0, nothing is drawn because it assumes air, and when the chip is greater than or equal to 0, the image corresponding to the chip is drawn in line 45.
The "$" symbol in line 45 means type conversion, and if you write "x $ float" for an int variable x, the value of x, which is an integer, will be converted to float, which is a decimal. Kuin makes a strict distinction between types, such that values of type int must be written like "5" and values of type float must be written like "5.0". If you need to handle different types, use the "$" operator to convert them.
When run, it will look like Figure 5-2.
Drawing The Map
Figure 5-2: Drawing The Map
If you open the contents of map_sample_side.txt in a text editor, you will see what is shown in Figure 5-3.
map_sample_side.txt
Figure 5-3: map_sample_side.txt
The format is "number of map chips per line (horizontal)" and "number of map chips per column (vertical)", separated by commas and line breaks, and then the values of the map chips are listed. This time, there are 26 lined up horizontally and 15 vertically. If you write it in this format, it can be read by the game@makeMap function.

6Jump And Collision Detection

Lastly, I will implement player jumping and collision detection. I will only change the contents of "while(wnd@act()) ... end while", so I will only extract that part (Figure 6-1).
  1.   while(wnd@act())
  2.     do @texBack.drawScale(0.0, 0.0, 1600.0, 900.0, 0.0, 0.0, 800.0, 450.0, draw@white)
  3.     if(input@pad(0, %left) >= 1)
  4.       do @player.veloX :- 0.8
  5.       do @player.toRight :: false
  6.     end if
  7.     if(input@pad(0, %right) >= 1)
  8.       do @player.veloX :+ 0.8
  9.       do @player.toRight :: true
  10.     end if
  11.     if(input@pad(0, %a) >= 1 & input@pad(0, %a) <= 6 & @player.hitBottom())
  12.       do @player.veloY :: -14.0
  13.     end if
  14.     do @player.veloY :+ 0.4
  15.     do @player.veloX :: lib@clampFloat(@player.veloX, -8.0, 8.0)
  16.     do @player.veloY :: lib@clampFloat(@player.veloY, -14.0, 14.0)
  17.     do @player.move(0.0)
  18.     do game@hitMapRect(@map, @player, getChipInfo, null)
  19.     do @player.update()
  20.  
  21.     func getChipInfo(chip: int, info: game@ChipInfo)
  22.       switch(chip)
  23.       case lib@intMin to - 1
  24.         do info.shape :: %none
  25.       case 0, 1
  26.         do info.shape :: %rect
  27.         do info.solidFriction :: 0.2
  28.       case 2
  29.         do info.shape :: %triLeftTop
  30.         do info.solidFriction :: 0.1
  31.       case 6
  32.         do info.shape :: %rect
  33.         do info.solidFriction :: 0.0
  34.       case 7
  35.         do info.shape :: %rect
  36.         do info.solidFriction :: 0.2
  37.         do info.repulsion :: 0.95
  38.       end switch
  39.       do info.fluidFriction :: 0.98
  40.     end func
  41.  
  42.     do @texPlayer.drawScale(@player.x - 64.0, @player.y - 64.0, 128.0, 128.0, pattern[draw@cnt() / 12 % 4], @player.toRight ?(128.0, 0.0), 64.0, 64.0, draw@white)
  43.     for y(0, 14)
  44.       for x(0, 25)
  45.         var chip: int :: @map.get(x, y)
  46.         if(chip >= 0)
  47.           do @texMap.drawScale(x $ float * 64.0 - 32.0, y $ float * 64.0 - 32.0, 64.0, 64.0, chip $ float * 32.0, 0.0, 32.0, 32.0, draw@white)
  48.         end if
  49.       end for
  50.     end for
  51.     do draw@render(60)
  52.   end while
Figure 6-1: kui_action6.kn
Lines 11 to 13 are the jump process. The "&" in line 11 is the equivalent of the English word "and". The condition of this if statement means that if the A button is pressed for more than or equal to 1 frame, and the A button is pressed for less than or equal to 6 frames, and the player is grounded, the player will jump. I have made the conditions somewhat complicated so that the player can still jump if the button is pressed just before landing. In line 12, the upward velocity is set to jump.
Line 14 is adding velocity downward, which is gravity. In lines 15-16, the maximum velocity is set. The lib@clampFloat function is a function that fits a value into a range, and the arguments in its parentheses are, in order, target value, minimum value, and maximum value.
In lines 18-19, the collision between the player and the map is evaluated. Collision detection is done in the following order: move, call each function of collision detection, and update. When you call each function of collision detection, the data of the collision result is accumulated and reflected in the position and velocity by the update function.
The game@hitMapRect on line 18 is a function to determine the collision between game@Map and game@Rect. The arguments in parentheses are, in order, the value of game@Map, the value of game@Rect, the function that returns the map chip information, and the function that is called when a collision occurs. Here I am passing the getChipInfo function that I am creating in lines 21-40 to the function that returns the map chip information, which includes the shape of the map chip, friction and air resistance.
When the getChipInfo function in line 21 is called, the kind of map chip and the variable for storing information are sent to chip and info in the parentheses, respectively. Based on the value of chip, information will be stored in info.
The switch in line 22 is an instruction that branches according to the value in its parentheses, and jumps to the case process where the value matches. For example, if the value in the parenthesis is 2, it will match "case 2" on line 28, so the program will run from there, and when it reaches another case on line 31, it will leave the switch statement and jump to line 39.
Multiple values can be written in case, separated by commas, such as "1, 3, 5", or a range, such as "3 to 10". In line 23, lib@intMin means the minimum value that the int type can handle, so "case lib@intMin to -1" means the range above the minimum value of int and below -1.
As for the values in info, info.shape is the shape of the map chip, info.solidFriction is the friction coefficient of the solid, info.repulsion is the repulsion coefficient when touched, and info.fluidFriction is the air resistance. Once they are set, the physical behavior of the collision is automatically calculated.
When run, it will look like Figure 6-2.
Action Game completion
Figure 6-2: Action Game completion
That's it, the size view action game is now complete. If you remove the gravity, you can also create a top view game. In the next article, I will explain how to create menus and how to handle saved data.
1660384110enf