How to Create a Curved Track in GameMaker

How do I create a curve in GameMaker? How do I create a track using this curve and the road texture? Do I need to know trigonometry for this? How do I use vector knowledge for this?

Hello everyone! This is programmer Alexander Kondyrev. In this article I will answer the questions listed above.

When GameMaker was acquired by YoYo Games, it got a new IDE and became GameMaker Studio. A colleague recently told me that GameMaker Studio 2 is now simply called GameMaker again. I had gotten used to calling it GMS2, ​​so I didn't notice the change. I'm correcting myself and now calling it GameMaker or GM again.

When I started programming games, I didn't understand how to use curves.

Objects built on curves seemed magical. I didn't realize one important concept back then: a human can't tell a curved line apart from a line broken into many small straight segments. I'll use this concept in this tutorial. We'll build a curved line and break it into straight segments to create a route from them.

First, I drew on paper the curve along which I wanted to build the road.

The square highlights a section of the road, which I will examine in more detail in the figure below.

-3

This is a close-up of a curved section of a square. You can see in the picture that I have broken this section into ten equal-length pieces. Above and below each piece are squares. Each square is divided by a dotted line, showing that they are made up of triangles. These shapes, although close to squares, are actually trapezoids.

They narrow where the curve is concave and widen where it is convex.

In the following diagram I will look at one fragment in more detail.

-4

AB and BC are two curve fragments. Note that in this diagram I have reduced the number of triangles per curve fragment from four to two. My road fragment constructed along the curve will consist of triangles A2A1B1 and B1B2A2. To construct them, I need to find the coordinates of points A1, A2, B1, B2.

Now I'll move on to GM to build a test curve there.

-5

To do this, I add a Path1 curve in a new project – in GameMaker they are called path or paths.

-6

I open Room1 and create a Path_1 layer in this room.

-7

In this layer I make the created curve Path1 active.

-8

A curve can be open at the beginning and end, or closed, where the end point is connected to the start point. For a circular path, I need a closed curve, so in the checkbox Closed I put a tick. The paths can consist of straight lines or curves that touch these straight lines. For smooth turns on the track, I choose the second option and press the button Smooth Curve. Next, I place the dots and move them around the room to build the desired curve. I show the resulting curve in the screenshot.

-9

I add Object1 to the project and create its instance in the room. I will write the code in this object from now on.

To draw the road I will use a vertex buffer. For this purpose, in the event Create I create the vertex format for this buffer and the buffer itself.

-10

From the third to the seventh line I create the vertex format. It will contain the vertex coordinates, texture coordinates and color. For this example I don't need color, and the vertex coordinates are only two – X and Y. However, in this tutorial I will use the standard GameMaker shader, so I leave the vertex format like this.

From lines nine to thirteen, I create a vertex buffer and transfer it to video memory using the function vertex_freeze. All these lines have already been described in the article About 3D graphics. I will add the calculated vertices to the buffer between lines ten and twelve.

Now we need to calculate the coordinates of points A1, A2, B1, B2. To do this, I will introduce several variables.

-11

In the second line, I create an array called lines_array to store the coordinates of the calculated vertices. I call it lines because the vertices will be stored in pairs, and thus they will form lines. In the third line, I store the value 64 in the variable width , which represents the width of the track. And in the fourth line, I find half of this length and store it in the variable width_half . In the fifth line, I create a variable pth and assign it the value of the created path Path1 . In the sixth line, I assign the value 8 to the variable part_size . I want each piece of the path to be approximately that many pixels. The more pixels there are in each piece, the more angular the road will be. The fewer pixels there are in each piece, the smoother the road will be, but it will require more triangles to draw it. I say “approximately” about the length of the piece, because I don't know the length of the entire curve, and it won't necessarily be a multiple of eight.

In line 7, I use path_get_length to get and store the length of the curve in the path_length variable. In line 8, I integer divide the curve length by the fragment length to get the number of fragments in the curve. In line 9, I divide the curve length by the number of fragments to adjust the length of each fragment. In line 10, I divide the fragment size by the trace width to find that size in texture coordinates and store it in the tex_frac variable.

Next I will use a loop to go through all the curve fragments, calculate the coordinates A1, A2, B1, B2 and save them in an array.

Here is another diagram of the trapezoid with the required vertices.

-12

In the code below, I calculate the vector AB. With its help, I find the vector to the left of it AA1, and the vector to the right of it AA2. Using these vectors, I calculate the coordinates of the desired points A1 and A2, and then save them in an array. In the subsequent steps of the loop, I repeat this procedure for all the remaining fragments of the curve.

-13

Let's take a closer look at the code. From the fourteenth to the nineteenth line, I calculate the coordinates of points A and B. This is done using the path_get_x and path_get_y functions. These functions need to be passed a curve and a value from zero to one. Zero denotes the beginning of the curve, one is its end. To find the desired value, you need to divide the index by the number of parts. In the loop, I go through all the calculated points, so the index of each point corresponds to the iterator i . Within each step of the loop, to calculate the vector, you need to find the coordinates of two points, so the index of the second point will be i+1 .

From the twentieth to the twenty-fourth line, I calculate the vector, find its length, and normalize it. The length of a normalized vector is always one. To find the vector, subtract the coordinates of point B from the coordinates of point A. The point_distance function calculates the length of the vector. Then the coordinates of the vector are divided by its

length to find the unit vector.

From the twenty-fifth to the thirty-second line, the left and right vectors are calculated. The angle between the unit vector and the vectors being sought is always 90 degrees, so there is no need to use trigonometric formulas here. I swap the coordinates of the unit vector and add a minus sign to one of the coordinates. This is how the left and right unit vectors are found. I multiply them by half the width of the trace to get the required length.

From the thirty-third to the thirty-seventh line, I add the found vectors to the coordinates of point A to obtain the coordinates of the sought points A1 and A2. I save the obtained values ​​in the array lines_array .

To display the track, I drew a texture of 16×16 pixels. In the settings, you must check the box Separate Texture Pageotherwise GameMaker will add this texture to the general texture page at compile time. If this happens, the calculations given in this guide will not work, and you will have to use another method.

-14

Next, you need to add vertices to the buffer. In addition to spatial coordinates, I will need texture coordinates. I will draw another diagram:

-15

This diagram shows a square that represents a texture. The square is divided into 8 equal segments by dotted lines. The top left corner of the texture has coordinates (0,0), the bottom right has coordinates (1,1). The center of the texture has coordinates (x=0.5, y=0.5). Earlier I calculated the variable tex_frac . This value represents the horizontal distance between the two dotted lines.

In the next snippet I finish building the vertex buffer.

-16

Here I loop through all the points again and form two triangles from them at each step, which make up a trapezoid. From row fifty-one to sixty-one I extract two adjacent lines from the array. For convenience, I save the coordinates of the lines in separate variables ax1, ay1, ax2, ay2, bx1, by1, bx2, by2 . At the current index i I get the first line, and at index i+1 I get the next one. Note the construction at line fifty-two: % is the “modulo” or “mod” operator, which returns the remainder of a division. Why is this construction used? In the last iteration of the loop, i+1 will go beyond the length of the array, which would cause an error. But the modulo operator returns 0 in this case, and no error occurs. This gives the first line from the array, which is exactly what is needed for the last value. Previously, I did not use this construction and resorted to additional conditions or other solutions, which complicated the code and its reading.

From line sixty-two to seventy I add the first triangle to the buffer

A1B1A2 . From the seventy-second to the eightieth line, the second triangle B1B2A2 is added to the buffer.

Adding vertices to a buffer was described in detail in the article About 3D Graphics.

It is common practice to use U and V instead of X and Y for texture coordinates, respectively. The V coordinate for points A1 and B1 will always be zero, and for points

A2 and B2 are equal to one. This can be seen in the diagram above. The U coordinate is calculated by multiplying the current index i or j by the texture fragment length tex_frac . You can see that the U values ​​will exceed one at some point, but thanks to the gpu_set_texrepeat(true) function being enabled, such values ​​are acceptable because the texture will be repeated in all directions. In the screenshot, the road_buff buffer is highlighted in blue. This means that I removed the var word when declaring this variable so that it is available not only in the Create event, but also in the Draw event.

All that remains is to draw the created buffer on the screen. Below is the code from the Draw event.

-17

In the second line, I save the texture pointer to the tex variable. It is returned by the sprite_get_texture function, which is passed the name of the sprite and the frame for which I need to get the texture pointer. I have one frame, so the frame value is zero.

In the third line I enable texture repeat as mentioned above. In the fourth line I disable filtering. This is not necessary, but I am using a pixel texture and do not want its colors interpolated. In the sixth line I use the function

vertex_submit I draw a vertex buffer on the screen. This function is passed the vertex buffer road_buff , pr_trianglelist means that in the passed buffer the vertices describe triangles, and tex is a pointer to the texture.

We start the compilation and see the rendered trace on the screen.

-18

In this article, I built a curved track in GameMaker and displayed it on the screen. In the process, I found out that basic knowledge of vectors is needed for building and knowledge of trigonometry is not required at all. The article turned out to be much longer than the previous ones. Write in the comments whether you found it too long or, on the contrary, just right.

Thank you for reading this article to the end. I hope you enjoyed it and learned something new. If so, please vote for it, maybe more people will know about it.

I am currently trying to publish one article per week. Follow me to stay up to date with new posts.

If you liked this article, I also recommend checking out other materials about programming in GameMaker:

  1. Article about shaders

  2. About 3D Graphics. Part 1 of 2

  3. About 3D Graphics. Part 2 of 2

Thank you for your attention!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *