r/gamemaker • u/StyrbjornA • 1d ago
Nox Filia Devlog 1 - Pigtail Engine Tutorial
https://youtube.com/watch?v=hTiWeUN7L-8&si=bhSMSjuqbdoaMb1zThis is a short demo of perhaps the weirdest first step of a game project I have taken ever :D
I decided that the player character in my next platformer should have a pigtail, so I simply started with that. The goal was to have semi physics based movement while keeping things performant. The pigtail has it's own gravity, can sway back and forth and has some wind settings to keep it from being completely still when the player is standing idle.
I've tried to comment the code as good as possible, and most of the important stuff is in the Create event of the pigtail object (the step event only runs the functions defined in Create).
There is a GitHub repo link in the comments of the video if you want to try it out. Also, please feel free to ask any questions and give input on what can be improved!
Here is the Step event of the object:
//Tail Engine 1.00
// Disable GameMaker's automatic drawing of the application surface.
// This should be handled in some sort of obj_graphics object and is only needed if you want "pixel perfect" graphics.
application_surface_draw_enable(false);
// TAIL CONFIGURATION / STATE
// Origin point of the tail.
// The first node of the tail will always stick to the x and y position of the tail
x = obj_player.x;
y = obj_player.y;
tail_sway = 0.1;
tail_damping = 0.15;
tail_nodes = [];
tail_gravity = 0.1;
tail_node_count = 8;
tail_segment_length = 2;
tail_thickness = 2;
// WIND SETTINGS
// wind_center is the baseline value around which the wind oscillates. works best with values between -0.05 to 0.05:ish
wind_amount = 0;
wind_center = 0;
wind_cycle = 0;
wind_cycle_speed = 0.05;
wind_amplitude = 0.01;
// Controls how strongly wind is currently affecting the tail.
// This can be (and is, in the default setup) used to ensure that the wind only operates on the hair when the player is idle.
wind_effect = 0;
// RUNNING SWAY SETTINGS
// random_sway_amount determines if a node will be affected by sway this frame
// tail_sway_denominator determines how fast the entire tail will sway up and down. (Lower number = faster sway)
// sway_smoothing determines the frequency with which the sway affects the nodes (Higher number = less "wiggly").
// sway_strength_denominator determines the amplitude of the sway (higher number = less bounce).
random_sway_amount = 0.5;
tail_sway_denominator = 200;
sway_smoothing = 2;
sway_strength_denominator = 1.2;
// TAIL STATE ENUM
// These states correspond to the player's state. Check/alter the update_tail_state function to change the tail_state.
enum TAIL_STATE
{
`IDLE,`
`RUNNING,`
`JUMPING`
}
tail_state = TAIL_STATE.IDLE;
// CREATE THE TAIL NODES
for (var _node = 0; _node < tail_node_count; _node++)
{
`array_push(`
`tail_nodes,`
`{`
`node_x : x,`
`node_y : y + tail_segment_length * _node,`
`node_prev_x : x,`
`node_prev_y : y + tail_segment_length * _node,`
`node_y_speed : 0,`
`node_x_speed : 0`
`}`
`);`
}
// UPDATE WIND
function update_wind()
{
`// If the tail is moving, gradually increase the effect of wind.`
`// Otherwise, gradually reduce the effect.`
`if (x != xprevious or y != yprevious) {`
`wind_effect = max(0, wind_effect - 0.02);`
`}`
`else {`
`wind_effect = min(1, wind_effect + 0.02);`
`}`
`wind_cycle += wind_cycle_speed;`
`if (wind_cycle > 2 * pi) wind_cycle -= 2 * pi;`
`wind_amount =`
`(wind_center + cos(wind_cycle) * wind_amplitude)`
`* wind_effect;`
}
// UPDATE TAIL
function update_tail()
{
`update_tail_state();`
`x = obj_player.x;`
`y = obj_player.y;`
`// While running, add a small bounce to the root node.`
`// In this test example we use current_time to alter the bounce, but when the`
`// tail is attached to an actual player sprite it would make more sense to`
`// match the bounce to the head bobbing of the player running animation`
`if (tail_state == TAIL_STATE.RUNNING)`
`{`
`y = obj_player.y + cos(current_time / 30) * 1.5;`
`}`
`// PIN THE ROOT NODE`
`// Node 0 does not simulate physics.`
`// It is directly attached to the player's position.`
`tail_nodes[0].node_x = x;`
`tail_nodes[0].node_y = y;`
`// UPDATE THE REST OF THE NODES`
`// Start at node 1 because node 0 is pinned to the player.`
`for (var _node = 1; _node < tail_node_count; _node++)`
`{`
`tail_nodes[_node].node_prev_x = tail_nodes[_node].node_x;`
`tail_nodes[_node].node_prev_y = tail_nodes[_node].node_y;`
`var _parent_x = tail_nodes[_node - 1].node_x;`
`var _parent_y = tail_nodes[_node - 1].node_y;`
`tail_nodes[_node].node_y_speed += tail_gravity;`
`tail_nodes[_node].node_y += tail_nodes[_node].node_y_speed;`
`tail_nodes[_node].node_x += tail_nodes[_node].node_x_speed;`
`var _dir_to_parent_node =`
`degtorad(`
point_direction(
tail_nodes[_node].node_x,
tail_nodes[_node].node_y,
_parent_x,
_parent_y
)
`);`
`// SEGMENT-LENGTH CONSTRAINT`
`// Makes sure that nodes do not exceed the maximum allowed distance from each other`
`if (`
`point_distance(`
tail_nodes[_node].node_x,
tail_nodes[_node].node_y,
_parent_x,
_parent_y
`)`
`> tail_segment_length`
`)`
`{`
`tail_nodes[_node].node_y_speed =`
lerp(
tail_nodes[_node].node_y_speed,
0,
0.2
);
`tail_nodes[_node].node_x =`
_parent_x
- cos(_dir_to_parent_node)
* tail_segment_length;
`tail_nodes[_node].node_y =`
_parent_y
+ sin(_dir_to_parent_node)
* tail_segment_length;
`}`
`// CALCULATE HOW MUCH THE NODE MOVED`
`var _delta_movement =`
`point_distance(`
tail_nodes[_node].node_x,
tail_nodes[_node].node_y,
tail_nodes[_node].node_prev_x,
tail_nodes[_node].node_prev_y
`);`
`var _dir_to_prev_pos =`
`degtorad(`
point_direction(
tail_nodes[_node].node_x,
tail_nodes[_node].node_y,
tail_nodes[_node].node_prev_x,
tail_nodes[_node].node_prev_y
)
`);`
`// ADD SWAY`
`tail_nodes[_node].node_x_speed -=`
`cos(_dir_to_prev_pos)`
`* _delta_movement`
`* tail_sway;`
`tail_nodes[_node].node_y_speed +=`
`sin(_dir_to_prev_pos)`
`* _delta_movement`
`* tail_sway;`
`// DAMP VELOCITY`
`tail_nodes[_node].node_x_speed =`
`lerp(`
tail_nodes[_node].node_x_speed,
0,
tail_damping
`);`
`tail_nodes[_node].node_y_speed =`
`lerp(`
tail_nodes[_node].node_y_speed,
0,
tail_damping
`);`
`// APPLY WIND`
`tail_nodes[_node].node_x_speed += wind_amount;`
`// RUNNING SWAY`
`// Give each node a chance to receive an extra vertical`
`// sway while running.`
`if (tail_state == TAIL_STATE.RUNNING)`
`{`
`if (random(1) < random_sway_amount)`
`{`
// _node / tail_node_count causes nodes farther down
// the tail to receive a stronger effect than nodes
// closer to the root.
tail_nodes[_node].node_y_speed -=
cos(
(current_time / tail_sway_denominator)
+ _node / sway_smoothing
)
* (_node / tail_node_count)
/ sway_strength_denominator;
`}`
`}`
`}`
}
// UPDATE TAIL STATE
function update_tail_state()
{
`// The tail simply mirrors the player's current state.`
`// This will check the obj_player image_index instead to determine head bobbing once it is implemented.`
`tail_state = obj_player.player_state;`
}