Snake-Like Body with Nodes & Triangles

Hey! In this tutorial, we’re going to make a simple snake-like body made up using nodes and drawn using triangles.

This is what we’ll achieve in this tutorial:

0.gif

So let’s get going!

This example was made in GameMaker Studio 2, but it should work in 1.4 as well.

Theory

We’ll make a 2D array that contains nodes in its first axis and its properties in the second. We’ll call that array body.

So, in body[n, m], n is the node, and m is the property you want to access. The properties will be its x/y position, direction, width and edges.

That array will contain the nodes with their positions. Each node will follow the node before it. Then, the body will be drawn using triangles.

Initialization (Create)

First of all, I’ll create an object for the body. I’ll call it oBody.

In its Create event, I’ll initialize some enums and variables:

//Constants
enum pr{ 
    xpos, ypos, dir, width, p0x, p0y, p1x, p1y
}

//Vars
nodes = 32;
node_dist = 8;

If you don’t know what enums are, read the Enums part on this page. With these enums, I am initializing the property types for the nodes.

nodes is how many nodes there will be in total, and node_dist is the distance between each node.

Now, we’ll initialize the body array:

//Array
for(var i=0; i<nodes; i++){ 
    body[i, pr.xpos] = x - node_dist*i; 
    body[i, pr.ypos] = y; 
    body[i, pr.dir] = 0; 
    body[i, pr.width] = 32;

    body[i, pr.p0x] = 0;
    body[i, pr.p0y] = 0;
    body[i, pr.p1x] = 0;
    body[i, pr.p1y] = 0;
}

This is pretty simple. This for loop initializes all the nodes and their properties in the array. So, in body[i, pr.xpos], i is the node, and pr.xpos is the x position (pr is the enum, xpos is its member).

If you want, you can add more properties for the nodes simply by making another enum member.

x – node_dist*i will place the nodes in a horizontal position, from right to left.

Movement (Step)

Now, we’ll make the body move towards the mouse. In its Step event, we’ll add this:

//Movement
var mDir = point_direction(body[0, pr.xpos], body[0, pr.ypos], mouse_x, mouse_y);

body[0, pr.dir] = mDir;
body[0, pr.xpos] += lengthdir_x(mouse_check_button(mb_left) * 4, mDir);
body[0, pr.ypos] += lengthdir_y(mouse_check_button(mb_left) * 4, mDir);

mDir will get the direction from the position from the first node towards the mouse. That direction will be applied to the first node.

Then, the first node will be moved towards that direction, by adding lengthdir values to it. The first argument in those functions is the speed, and the second is the direction. So, it will move if you click the left mouse button.

So, this will move the first node. But what about the rest? Well, basically, each node will follow the one before it. So, node 1 will follow node 0, then node 2 will follow node 1, and so on.

This for loop will handle it all:

//Nodes
for(var i=1; i<nodes; i++){
    body[i, pr.dir] = point_direction(body[i, pr.xpos], body[i, pr.ypos], body[i-1, pr.xpos], body[i-1, pr.ypos]);

    body[i, pr.xpos] = body[i-1, pr.xpos] + lengthdir_x(node_dist, body[i, pr.dir]-180);
    body[i, pr.ypos] = body[i-1, pr.ypos] + lengthdir_y(node_dist, body[i, pr.dir]-180);
}

You can see that the loop starts from 1 instead of 0, because we’ve already handled the first node.

The first line in the loop sets the node’s direction. It gets the direction from the current node towards the previous node. So, it would face the node before it.

The next lines set the position of the node. The position for the current node is calculated from the position of the previous node. Using the lengthdir functions, the current node is placed node_dist distance away from the previous node, in the direction opposite to where the current node is facing (calculated by subtracting 180 from it).

Edges

Now, we’ll calculate the edges for the nodes. For that, we’ll add this for loop in the Step event:

//Edges
for(var i=0; i<nodes; i++){
 body[i, pr.p0x] = body[i, pr.xpos] + lengthdir_x(body[i, pr.width]/2, body[i, pr.dir]-90);
 body[i, pr.p0y] = body[i, pr.ypos] + lengthdir_y(body[i, pr.width]/2, body[i, pr.dir]-90);

 body[i, pr.p1x] = body[i, pr.xpos] + lengthdir_x(body[i, pr.width]/2, body[i, pr.dir]+90);
 body[i, pr.p1y] = body[i, pr.ypos] + lengthdir_y(body[i, pr.width]/2, body[i, pr.dir]+90);
}

For each node in the loop, two edges are calculated: p0(x/y) and p1(x/y). The distance between those edges is governed by the width property of that node.

One edge of a node is placed width/2 distance away from it, in dir-90 direction (right). The other is placed width/2 distance away in dir+90 (left) direction.

Triangles (Draw)

Now, we’ll handle the drawing of the body. We’ll add this in the Draw event:

//Draw with triangles
for(var i=1; i<nodes; i++){
 draw_triangle(body[i-1, pr.p0x], body[i-1, pr.p0y], body[i-1, pr.p1x], body[i-1, pr.p1y], body[i, pr.p0x], body[i, pr.p0y], false);
 draw_triangle(body[i, pr.p1x], body[i, pr.p1y], body[i-1, pr.p1x], body[i-1, pr.p1y], body[i, pr.p0x], body[i, pr.p0y], false);
}

Here, 2 triangles are drawn. They are drawn at the edges of the current node and the previous node.

Here’s a visual representation of how it’ll work:

nodes.png

The blue points are the nodes. The arrows show their directions. The red points are the edges.

So, from the nodes, the edges will be calculated (in Step). Then triangles will be drawn using those points (in Draw).

Result

0.gif

If you place the object in a room and run the game, you can see that it follows the mouse.

Skeleton (Draw)

If you want to see the skeleton of the body, add this code inside the for loop in the Draw event, at the end:

 //skeleton triangles
 draw_set_color(c_red);
 draw_triangle(body[i-1, pr.p0x], body[i-1, pr.p0y], body[i-1, pr.p1x], body[i-1, pr.p1y], body[i, pr.p0x], body[i, pr.p0y], true);
 draw_triangle(body[i, pr.p1x], body[i, pr.p1y], body[i-1, pr.p1x], body[i-1, pr.p1y], body[i, pr.p0x], body[i, pr.p0y], true);
 
 //skeleton nodes
 draw_set_color(c_blue);
 draw_circle(body[i, pr.xpos], body[i, pr.ypos], 2, false);

 //skeleton lines
 draw_line(body[i, pr.xpos], body[i, pr.ypos], body[i-1, pr.xpos], body[i-1, pr.ypos]);
 draw_set_color(c_white);

This will draw the skeleton with the triangles in red and the nodes in blue.

2.gif

Collisions

For collisions, you can use this script:

/// body_collision(object);
/// @arg object
var obj = argument0;

for(var i=1; i<nodes; i++){
 //collision
 var col = false;

 var _body = body;

 with(obj){
 if (rectangle_in_triangle(bbox_left, bbox_top, bbox_right, bbox_bottom,
 _body[i-1, pr.p0x], _body[i-1, pr.p0y], _body[i-1, pr.p1x], _body[i-1, pr.p1y], _body[i, pr.p0x], _body[i, pr.p0y])
 ||
 rectangle_in_triangle(bbox_left, bbox_top, bbox_right, bbox_bottom,
 _body[i, pr.p1x], _body[i, pr.p1y], _body[i-1, pr.p1x], _body[i-1, pr.p1y], _body[i, pr.p0x], _body[i, pr.p0y]))
 {
  col = true;
 }
 }

 //return true if collision is found
 if (col) return true;
}

return false;

Basically, this runs a loop similar to the one in the Draw event. But instead of drawing the triangles, it checks if an object’s hitbox is touching the triangles. If it is, it returns true.

To check the hitbox, it uses the function rectangle_in_triangle() which checks whether a rectangle is touching a triangle. The rectangle here is made up using the object’s hitbox coordinates (bbox_left, bbox_top, bbox_right and bbox_bottom) and the triangles are the same triangles that are drawn.

So, you can use this like a condition:

if (body_collision(oRock)){
 //do something on collision
}

Conclusion

Hope this tutorial helped!

Download this project here (GMS2)

With this method, you can make amazing stuff, like this dragon I made:

BURNBURNBURN.gif


If you’re looking to improve your games, check out the assets I have to offer. Here’s one of my assets, a water shader:

refl1.gif
(The GIF might appear shaky in the lower half. It’s WordPress’s problem.)

If you encounter any problems with this tutorial or just want to ask something, contact me on Discord through this server (my username is Senpai Matharoo).

See you later, and till then, happy dev’ing!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s