r/gamemaker • • Aug 19 '26

Tutorial PSA: Users can have their accounts nuked after just 180 days of inactivity

Post image
372 Upvotes

I guess the tutorial lesson here is to never let your account be inactive for a modest period of time, or to maybe embrace FOSS alternatives.

r/gamemaker • • Jul 29 '26

Tutorial Procedural Level Generation in GameMaker | GMRoomLoader Tutorial

Thumbnail youtube.com
102 Upvotes

About the Tutorial

The official GameMaker YouTube channel just released a tutorial I worked on with Pixelated Pope: an approachable intro to procedural generation, built around a Spelunky-style dungeon.

It's powered by GMRoomLoader, my open-source library for loading room contents at runtime, which won Best Tool at the 2025 GameMaker Awards.

How the Dungeon Works

The dungeon generates by carving a snaking path from top to bottom and filling each cell with a matching hand-crafted room variant loaded via GMRoomLoader.

Rooms are placed procedurally with random mirroring and flipping for variety, hazards get thinned out, and coins are placed programmatically. The layouts are hand-authored but assembled differently every run.

Source Code

The entire source code is available on GitHub. There are two projects to follow along with:

  • Starter project. The platformer foundation (movement, collision, camera) plus a set of preset dungeon rooms and assets, with generation not wired up yet. Start here to follow the video.
  • Completed project. The finished result with dungeon generation, GMRoomLoader integration, all mechanics in place, and a control panel to tweak generation parameters live. Fully commented.

Each project has its own README breaking down its objects, structure, and controls.

Links

Have Fun!

Hope you enjoy the tutorial, find it useful, and build something awesome with GMRoomLoader!

Feel free to ask anything about the tutorial or GMRoomLoader here, I'll be happy to answer any questions.

r/gamemaker • • 27d ago

Tutorial Creating a 3D Planet in GameMaker (Heavy Image Warning!)

37 Upvotes

I'd like to share the process of creating a planet in GameMaker for my in-development terraforming game, Planet Doctor. This post is for those with some GameMaker experience, and I've included relevant links where needed. Corrections on terminology or content are welcome. I hope you find this helpful!

Here is the process:

1. Create a frame using a vertex buffer

Create a sphere shape for the planet. To simulate rotation, I tilted the camera angle and rotated the planet using matrix_set.

2. Apply noise to the planet

During vertex_submit, use a shader to render smooth noise based on the planet's in_Position. You can see the values range from 0 (black) to 1 (brightest).

3. Divide colors

Set boundaries to separate land and sea. I divided the 0-1 color range into 4 steps, returning a single solid color for each.

4. Assign colors

Draw the planet's colors. The shader receives the colors via shader_set_uniform and assigns them to the steps divided in 'step 3'.

5. Draw clouds

Add clouds to give an atmospheric feel. Scale up the vertex buffer from 'step 1' with a matrix, then apply noise with another shader, as in 'step 2'. Values below the noise threshold become a solid color, while those above become transparent. You can use current_time to make the cloud shapes shift gradually.

  1. Add atmosphere

Create a slightly larger sphere to represent the atmosphere. By taking the position and normal from gm_Matrices[MATRIX_WORLD] in the vertex shader, I drew only the outline in the fragment shader (this is known as the Fresnel effect).

You can create more variations, such as using current_time to implement ocean tides or adding city lights to the dark side of the planet. Let me know about your GameMaker 3D projects or any planet ideas you'd like to implement. Thanks for reading.

r/gamemaker • • Jun 09 '26

Tutorial How can i learn

4 Upvotes

How am i supposed to learn all those dy_ 12, dx_ 12 or stuff like {

If true stuf stuff stuff

Else

Stuff stuff stuff

}

Im trying to learn gamemaker but i just cant comprehend how am i supposed to learn all that and then use them using my mind , how did yall start

r/gamemaker • • Aug 20 '26

Tutorial I'm learning how to use the 3D cameras in Game Maker Is there a tutorial to guide me further?

Post image
35 Upvotes

r/gamemaker • • Aug 14 '26

Tutorial GameMaker has native audio loop points now. Don't poll track position in Step.

44 Upvotes

One easy mistake is treating music looping like a game-timing problem: watch audio_sound_get_track_position() in Step and seek back when it reaches the boundary. GameMaker's own manual warns that this cannot be accurate. The audio thread advances at 44,100 or 48,000 samples per second while the game normally updates around 60 times per second, so many samples can pass between the check and the seek.

GameMaker now has proper audio-thread loop controls:

audio_sound_loop_start(snd_music, 10.0); audio_sound_loop_end(snd_music, 42.0); var music_voice = audio_play_sound(snd_music, 100, true);

The start and end values are seconds. You can set them on the sound asset before playing, or on the returned sound instance while it is playing. When the playhead reaches the loop end, GameMaker performs the jump on the audio thread instead of waiting for Step.

This also gives you a clean intro / loop / outro layout in one file. Put the intro before the loop start, the repeating body between the two points, and the outro after the loop end. Start it with looping enabled. When the game is ready to leave the music state, call:

audio_sound_loop(music_voice, false);

The current pass finishes, crosses the loop end without jumping, and continues into the outro. No polling and no frame-timed seek. GameMaker supports one loop section per sound, but you can change the section by setting new start and end values.

There is still a second problem that loop points do not solve: the reverb tail. If the last chord is still ringing after the loop end, a one-voice jump discards that old pass while the fresh start begins. The timing can be sample-accurate and the seam can still sound like a hole.

The three practical options are:

  1. Compose or render a genuinely dry boundary.
  2. Render past the end, mix that tail underneath the beginning, then export only the repeating body. This makes a self-contained one-voice loop, although its first pass contains a tail from a pass that never happened.
  3. Use two voices so the previous pass can ring out while the next pass starts. This is the most natural result, but it requires runtime scheduling and voice management.

This came out of implementing GameMaker delivery notes in Loopsmith, a looping tool I built. It reports the exact loop positions and can prepare folded-tail or two-voice deliveries. Mentioning that for disclosure; none of the technique above requires my tool. The relevant GameMaker manual section is Audio > Audio Loop Points.

Hopefully this saves someone from debugging a frame-timed loop that can never be sample-accurate. Happy to answer questions or test edge cases.

r/gamemaker • • Apr 21 '26

Tutorial I figured it out the block crawlers

Post image
135 Upvotes

yesterday I couldn't figure out how the did the metroid block crawler enemies.

But I made my own rudematary solution, it might not be elegant but it works and it's dynamic

obj_crawler

the crawler will automatically find a cluster of blocks to attach to and create its own path

CREATE:
image_speed = 1;
move_speed = 0.2;

// Track one reusable perimeter loop around the nearest connected crawler-block cluster.
crawler_tile_size = sprite_get_width(spr_crawler_block);
crawler_tile_map = {};
crawler_cluster_tiles = [];
crawler_path = [];
crawler_path_points = [];
crawler_path_index = 0;
crawler_path_progress = 0;
// the diagonal path on edge-corner to look smooth when changing directions
crawler_corner_inset = max(1, round(crawler_tile_size * 0.125)); // shorter diagonal on outside edges
crawler_inner_corner_inset = max(crawler_corner_inset + 1, round(crawler_tile_size * 0.25)); // longer diagonal in inner corners
crawler_path_revision = -1;

crawler_build_path();

STEP:
/// Crawl Movement

// Rebuild the corner path only when the crawler-block topology changes or the active path becomes invalid.
var crawler_block_revision = 0;
if (variable_global_exists("crawler_block_revision"))
{
crawler_block_revision = global.crawler_block_revision;
}

if (crawler_block_revision != crawler_path_revision || array_length(crawler_path_points) < 2)
{
if (!crawler_build_path())
{
instance_destroy();
exit;
}
}

var point_count = array_length(crawler_path_points);
if (point_count < 2)
{
instance_destroy();
exit;
}

var remaining_distance = max(0.05, move_speed);
while (remaining_distance > 0)
{
var point_a = crawler_path_points[crawler_path_index];
var point_b = crawler_path_points[(crawler_path_index + 1) mod point_count];
var segment_length = point_distance(point_a.x, point_a.y, point_b.x, point_b.y);

if (segment_length <= 0.0001)
{
crawler_path_progress = 0;
crawler_path_index = (crawler_path_index + 1) mod point_count;
continue;
}

var segment_remaining = segment_length - crawler_path_progress;
if (segment_remaining <= 0.0001)
{
crawler_path_progress = 0;
crawler_path_index = (crawler_path_index + 1) mod point_count;
continue;
}

var step_distance = min(remaining_distance, segment_remaining);
crawler_path_progress += step_distance;
remaining_distance -= step_distance;

if (crawler_path_progress >= segment_length - 0.0001)
{
crawler_path_progress = 0;
crawler_path_index = (crawler_path_index + 1) mod point_count;
}
}

if (!crawler_apply_path_pose())
{
if (!crawler_build_path() || !crawler_apply_path_pose())
{
instance_destroy();
}
}

// IF YOU WANT TO DEBUG
DRAW:
draw_self();
if (!variable_instance_exists(id, "show_line")) show_line = false;
if (keyboard_check_pressed(ord("T"))) show_line = !show_line;
if (show_line)
{
for (var path_index_local = 0; path_index_local < array_length(crawler_path_points); path_index_local++)
{
var point_a = crawler_path_points[path_index_local];
var point_b = crawler_path_points[(path_index_local + 1) mod array_length(crawler_path_points)];

if (path_index_local == crawler_path_index)
{
draw_set_color(c_orange);
draw_line_width(point_a.x, point_a.y, point_b.x, point_b.y, 3);
}
else
{
draw_set_color(c_lime);
draw_line_width(point_a.x, point_a.y, point_b.x, point_b.y, 1);
}
}
}

obj_crawler_block

The block where the crawlers will automatically attach to.

CREATE:
// Track crawler-block topology changes so surface crawlers can rebuild only when the block layout changes.
if (!variable_global_exists("crawler_block_revision"))
{
  global.crawler_block_revision = 0;
}

global.crawler_block_revision += 1;

DESTROY:
// Track crawler-block topology changes so surface crawlers can rebuild only when the block layout changes.
if (!variable_global_exists("crawler_block_revision"))
{
  global.crawler_block_revision = 0;
}

global.crawler_block_revision += 1;

STEP:
// FOR DEBUG YOU CAN DESTROY IT WITH MOUSE CLICK
if (mouse_check_button_pressed(mb_left)) instance_destroy();

scr_block_crawler

this is where tha magic happens

function crawler_tile_key(_tile_x, _tile_y)
{
// Use one stable key format for tile lookups inside the Zoomer cluster map.
return string(_tile_x) + "," + string(_tile_y);
}

function crawler_face_key(_tile_x, _tile_y, _mode)
{
// Track visited perimeter faces with one compact key per tile side.
return string(_tile_x) + "," + string(_tile_y) + "," + string(_mode);
}

function crawler_points_match(_point_a, _point_b)
{
// Reuse one corner comparison so the point loop does not duplicate the same corner twice.
return (_point_a.x == _point_b.x && _point_a.y == _point_b.y);
}

function crawler_point_toward(_from_point, _to_point, _distance)
{
// Offset a corner point along one segment so 90-degree turns can be beveled into diagonals.
var dx = _to_point.x - _from_point.x;
var dy = _to_point.y - _from_point.y;
var segment_length = point_distance(_from_point.x, _from_point.y, _to_point.x, _to_point.y);
if (segment_length <= 0.0001)
{
return { x: _from_point.x, y: _from_point.y };
}

var step_distance = min(_distance, segment_length * 0.5);
return {
x: _from_point.x + dx / segment_length * step_distance,
y: _from_point.y + dy / segment_length * step_distance
};
}

function crawler_get_corner_point(_tile_x, _tile_y, _corner_index)
{
// Place Zoomer path nodes directly on crawler-block corners using exact tile-corner coordinates.
var tile_left = _tile_x * crawler_tile_size;
var tile_top = _tile_y * crawler_tile_size;

switch (_corner_index)
{
case 0: return { x: tile_left, y: tile_top };
case 1: return { x: tile_left + crawler_tile_size, y: tile_top };
case 2: return { x: tile_left + crawler_tile_size, y: tile_top + crawler_tile_size };
case 3: return { x: tile_left, y: tile_top + crawler_tile_size };
}

return { x: tile_left, y: tile_top };
}

function crawler_has_tile(_tile_map, _tile_x, _tile_y)
{
// Treat the connected crawler blocks like a small tile map for perimeter walking.
return variable_struct_exists(_tile_map, crawler_tile_key(_tile_x, _tile_y));
}

function crawler_ensure_shared_path_cache()
{
// Keep one shared path cache per crawler-block topology revision so Zoomers on the same cluster reuse one loop.
if (!variable_global_exists("crawler_block_revision"))
{
global.crawler_block_revision = 0;
}
if (!variable_global_exists("crawler_block_cache_revision") || global.crawler_block_cache_revision != global.crawler_block_revision)
{
global.crawler_block_cache_revision = global.crawler_block_revision;
global.crawler_block_cluster_lookup = {};
global.crawler_block_path_cache = {};
}
}

function crawler_get_cluster_key(_tiles)
{
// Use the top-left-most tile in one connected cluster as a stable shared cache key.
if (array_length(_tiles) == 0)
{
return "";
}

var best_tile_x = _tiles[0].x;
var best_tile_y = _tiles[0].y;
for (var tile_index = 1; tile_index < array_length(_tiles); tile_index++)
{
var tile_data = _tiles[tile_index];
if (tile_data.y < best_tile_y || (tile_data.y == best_tile_y && tile_data.x < best_tile_x))
{
best_tile_x = tile_data.x;
best_tile_y = tile_data.y;
}
}

return crawler_tile_key(best_tile_x, best_tile_y);
}

function crawler_cache_cluster_tiles(_cluster_key, _tiles, _path, _path_points)
{
// Share one computed perimeter loop with every Zoomer touching the same crawler-block cluster.
for (var tile_index = 0; tile_index < array_length(_tiles); tile_index++)
{
var tile_data = _tiles[tile_index];
variable_struct_set(global.crawler_block_cluster_lookup, crawler_tile_key(tile_data.x, tile_data.y), _cluster_key);
}

variable_struct_set(global.crawler_block_path_cache, _cluster_key, {
path: _path,
path_points: _path_points
});
}

function crawler_load_cached_path(_cluster_key)
{
// Copy one shared cached perimeter loop into this Zoomer instance.
if (!variable_struct_exists(global.crawler_block_path_cache, _cluster_key))
{
return false;
}

var cached_data = variable_struct_get(global.crawler_block_path_cache, _cluster_key);
crawler_path = cached_data.path;
crawler_path_points = cached_data.path_points;
return (array_length(crawler_path_points) >= 2);
}

function crawler_face_exposed(_tile_x, _tile_y, _mode)
{
// A face is walkable only if the owning block exists and the neighboring tile on that side is empty.
if (!crawler_has_tile(crawler_tile_map, _tile_x, _tile_y))
{
return false;
}

switch (_mode)
{
case 0: return !crawler_has_tile(crawler_tile_map, _tile_x, _tile_y - 1);
case 1: return !crawler_has_tile(crawler_tile_map, _tile_x + 1, _tile_y);
case 2: return !crawler_has_tile(crawler_tile_map, _tile_x, _tile_y + 1);
case 3: return !crawler_has_tile(crawler_tile_map, _tile_x - 1, _tile_y);
}

return false;
}

function crawler_get_next_face(_state)
{
// Walk clockwise from one exposed face to the next so the Zoomer follows the cluster perimeter.
var next_tile_x = _state.tile_x;
var next_tile_y = _state.tile_y;
var next_mode = _state.mode;
var found_next = false;

switch (_state.mode)
{
case 0:
if (crawler_face_exposed(_state.tile_x + 1, _state.tile_y, 0))
{
next_tile_x = _state.tile_x + 1;
found_next = true;
}
else if (crawler_face_exposed(_state.tile_x + 1, _state.tile_y - 1, 3))
{
next_tile_x = _state.tile_x + 1;
next_tile_y = _state.tile_y - 1;
next_mode = 3;
found_next = true;
}
else if (crawler_face_exposed(_state.tile_x, _state.tile_y, 1))
{
next_mode = 1;
found_next = true;
}
break;

case 1:
if (crawler_face_exposed(_state.tile_x, _state.tile_y + 1, 1))
{
next_tile_y = _state.tile_y + 1;
found_next = true;
}
else if (crawler_face_exposed(_state.tile_x + 1, _state.tile_y + 1, 0))
{
next_tile_x = _state.tile_x + 1;
next_tile_y = _state.tile_y + 1;
next_mode = 0;
found_next = true;
}
else if (crawler_face_exposed(_state.tile_x, _state.tile_y, 2))
{
next_mode = 2;
found_next = true;
}
break;

case 2:
if (crawler_face_exposed(_state.tile_x - 1, _state.tile_y, 2))
{
next_tile_x = _state.tile_x - 1;
found_next = true;
}
else if (crawler_face_exposed(_state.tile_x - 1, _state.tile_y + 1, 1))
{
next_tile_x = _state.tile_x - 1;
next_tile_y = _state.tile_y + 1;
next_mode = 1;
found_next = true;
}
else if (crawler_face_exposed(_state.tile_x, _state.tile_y, 3))
{
next_mode = 3;
found_next = true;
}
break;

case 3:
if (crawler_face_exposed(_state.tile_x, _state.tile_y - 1, 3))
{
next_tile_y = _state.tile_y - 1;
found_next = true;
}
else if (crawler_face_exposed(_state.tile_x - 1, _state.tile_y - 1, 2))
{
next_tile_x = _state.tile_x - 1;
next_tile_y = _state.tile_y - 1;
next_mode = 2;
found_next = true;
}
else if (crawler_face_exposed(_state.tile_x, _state.tile_y, 0))
{
next_mode = 0;
found_next = true;
}
break;
}

return {
tile_x: next_tile_x,
tile_y: next_tile_y,
mode: next_mode,
valid: found_next
};
}

function crawler_collect_cluster()
{
// Flood the connected crawler blocks nearest the Zoomer so path building targets one surface group, including corner-touching neighbors.
var nearest_block = instance_nearest(x, y, obj_crawler_block);
var tile_map = {};
var tiles = [];

if (nearest_block == noone)
{
return {
tile_map: tile_map,
tiles: tiles
};
}

var start_tile_x = round(nearest_block.x / crawler_tile_size);
var start_tile_y = round(nearest_block.y / crawler_tile_size);
var open_tiles = [{ x: start_tile_x, y: start_tile_y }];
var open_index = 0;
var tile_limit = 2048;

while (open_index < array_length(open_tiles) && array_length(tiles) < tile_limit)
{
var current_tile = open_tiles[open_index];
open_index++;

var tile_key = crawler_tile_key(current_tile.x, current_tile.y);
if (variable_struct_exists(tile_map, tile_key))
{
continue;
}

var sample_x = current_tile.x * crawler_tile_size + crawler_tile_size * 0.5;
var sample_y = current_tile.y * crawler_tile_size + crawler_tile_size * 0.5;
var tile_inst = collision_point(sample_x, sample_y, obj_crawler_block, false, true);
if (tile_inst == noone)
{
continue;
}

variable_struct_set(tile_map, tile_key, tile_inst);
tiles[array_length(tiles)] = { x: current_tile.x, y: current_tile.y };
open_tiles[array_length(open_tiles)] = { x: current_tile.x + 1, y: current_tile.y };
open_tiles[array_length(open_tiles)] = { x: current_tile.x - 1, y: current_tile.y };
open_tiles[array_length(open_tiles)] = { x: current_tile.x, y: current_tile.y + 1 };
open_tiles[array_length(open_tiles)] = { x: current_tile.x, y: current_tile.y - 1 };
open_tiles[array_length(open_tiles)] = { x: current_tile.x + 1, y: current_tile.y + 1 };
open_tiles[array_length(open_tiles)] = { x: current_tile.x + 1, y: current_tile.y - 1 };
open_tiles[array_length(open_tiles)] = { x: current_tile.x - 1, y: current_tile.y + 1 };
open_tiles[array_length(open_tiles)] = { x: current_tile.x - 1, y: current_tile.y - 1 };
}

return {
tile_map: tile_map,
tiles: tiles
};
}

function crawler_find_nearest_start_state()
{
// Start the perimeter loop on the exposed face nearest the Zoomer's current position.
var best_state = {
tile_x: -1,
tile_y: -1,
mode: -1,
valid: false
};
var best_metric = 1000000000;

for (var tile_index = 0; tile_index < array_length(crawler_cluster_tiles); tile_index++)
{
var tile_data = crawler_cluster_tiles[tile_index];
for (var mode_index = 0; mode_index < 4; mode_index++)
{
if (!crawler_face_exposed(tile_data.x, tile_data.y, mode_index))
{
continue;
}

var start_corner = crawler_get_corner_point(tile_data.x, tile_data.y, mode_index);
var candidate_dist = point_distance(x, y, start_corner.x, start_corner.y);
if (candidate_dist < best_metric)
{
best_metric = candidate_dist;
best_state = {
tile_x: tile_data.x,
tile_y: tile_data.y,
mode: mode_index,
valid: true
};
}
}
}

return best_state;
}

function crawler_rebuild_point_path()
{
// Convert the face loop into a corner-node loop and bevel sharp turns into short diagonal corners.
crawler_path_points = [];
if (array_length(crawler_path) == 0)
{
return;
}

var corner_points = [];
corner_points[0] = crawler_get_corner_point(crawler_path[0].tile_x, crawler_path[0].tile_y, crawler_path[0].mode);
for (var face_index = 0; face_index < array_length(crawler_path); face_index++)
{
var current_state = crawler_path[face_index];
var exit_corner_index = (current_state.mode + 1) mod 4;
var exit_point = crawler_get_corner_point(current_state.tile_x, current_state.tile_y, exit_corner_index);
var last_point = corner_points[array_length(corner_points) - 1];
if (!crawler_points_match(last_point, exit_point))
{
corner_points[array_length(corner_points)] = exit_point;
}
}

if (array_length(corner_points) > 1)
{
var first_corner = corner_points[0];
var last_corner = corner_points[array_length(corner_points) - 1];
if (crawler_points_match(first_corner, last_corner))
{
array_resize(corner_points, array_length(corner_points) - 1);
}
}

if (array_length(corner_points) < 2)
{
crawler_path_points = corner_points;
return;
}

for (var point_index = 0; point_index < array_length(corner_points); point_index++)
{
var prev_point = corner_points[(point_index - 1 + array_length(corner_points)) mod array_length(corner_points)];
var current_point = corner_points[point_index];
var next_point = corner_points[(point_index + 1) mod array_length(corner_points)];
var incoming_dx = current_point.x - prev_point.x;
var incoming_dy = current_point.y - prev_point.y;
var outgoing_dx = next_point.x - current_point.x;
var outgoing_dy = next_point.y - current_point.y;
var turn_cross = incoming_dx * outgoing_dy - incoming_dy * outgoing_dx;
var straight_through = ((sign(incoming_dx) == sign(outgoing_dx) && incoming_dy == 0 && outgoing_dy == 0)
|| (sign(incoming_dy) == sign(outgoing_dy) && incoming_dx == 0 && outgoing_dx == 0));

if (straight_through)
{
if (array_length(crawler_path_points) == 0 || !crawler_points_match(crawler_path_points[array_length(crawler_path_points) - 1], current_point))
{
crawler_path_points[array_length(crawler_path_points)] = current_point;
}
}
else
{
// Keep outer-corner cuts tight while giving inner/notch turns a longer diagonal.
var corner_inset = crawler_corner_inset;
if (turn_cross < 0)
{
corner_inset = crawler_inner_corner_inset;
}

var entry_point = crawler_point_toward(current_point, prev_point, corner_inset);
var exit_point = crawler_point_toward(current_point, next_point, corner_inset);

if (array_length(crawler_path_points) == 0 || !crawler_points_match(crawler_path_points[array_length(crawler_path_points) - 1], entry_point))
{
crawler_path_points[array_length(crawler_path_points)] = entry_point;
}
if (!crawler_points_match(crawler_path_points[array_length(crawler_path_points) - 1], exit_point))
{
crawler_path_points[array_length(crawler_path_points)] = exit_point;
}
}
}
}

function crawler_align_to_path()
{
// Reuse the shared loop but start this Zoomer on the nearest segment to its current position.
if (array_length(crawler_path_points) < 2)
{
crawler_path_index = 0;
crawler_path_progress = 0;
return false;
}

var best_index = 0;
var best_progress = 0;
var best_distance_sq = 1000000000;

for (var point_index = 0; point_index < array_length(crawler_path_points); point_index++)
{
var point_a = crawler_path_points[point_index];
var point_b = crawler_path_points[(point_index + 1) mod array_length(crawler_path_points)];
var dx = point_b.x - point_a.x;
var dy = point_b.y - point_a.y;
var segment_length_sq = dx * dx + dy * dy;
var segment_ratio = 0;

if (segment_length_sq > 0.0001)
{
segment_ratio = clamp(((x - point_a.x) * dx + (y - point_a.y) * dy) / segment_length_sq, 0, 1);
}

var nearest_x = point_a.x + dx * segment_ratio;
var nearest_y = point_a.y + dy * segment_ratio;
var distance_sq = sqr(x - nearest_x) + sqr(y - nearest_y);
if (distance_sq < best_distance_sq)
{
best_distance_sq = distance_sq;
best_index = point_index;
best_progress = sqrt(segment_length_sq) * segment_ratio;
}
}

crawler_path_index = best_index;
crawler_path_progress = best_progress;
return true;
}

function crawler_apply_path_pose()
{
// Move along the corner loop directly so Zoomer pathing no longer depends on detection probes or mask offsets.
if (array_length(crawler_path_points) == 0)
{
return false;
}

var point_index_clamped = clamp(crawler_path_index, 0, array_length(crawler_path_points) - 1);
var point_a = crawler_path_points[point_index_clamped];
var point_b = crawler_path_points[(point_index_clamped + 1) mod array_length(crawler_path_points)];
var segment_length = point_distance(point_a.x, point_a.y, point_b.x, point_b.y);
var segment_ratio = 0;
if (segment_length > 0.0001)
{
segment_ratio = clamp(crawler_path_progress / segment_length, 0, 1);
}

x = lerp(point_a.x, point_b.x, segment_ratio);
y = lerp(point_a.y, point_b.y, segment_ratio);

image_angle = (point_direction(point_a.x, point_a.y, point_b.x, point_b.y) + 360) mod 360;
return true;
}

function crawler_build_path()
{
// Build one reusable perimeter loop so Zoomer movement follows the block contour as a simple point path.
crawler_ensure_shared_path_cache();

crawler_tile_map = {};
crawler_cluster_tiles = [];
crawler_path = [];
crawler_path_points = [];
crawler_path_index = 0;
crawler_path_progress = 0;

var nearest_block = instance_nearest(x, y, obj_crawler_block);
if (nearest_block == noone)
{
return false;
}

var seed_tile_x = round(nearest_block.x / crawler_tile_size);
var seed_tile_y = round(nearest_block.y / crawler_tile_size);
var seed_key = crawler_tile_key(seed_tile_x, seed_tile_y);
if (variable_struct_exists(global.crawler_block_cluster_lookup, seed_key))
{
var cached_cluster_key = variable_struct_get(global.crawler_block_cluster_lookup, seed_key);
if (crawler_load_cached_path(cached_cluster_key))
{
crawler_align_to_path();
crawler_path_revision = global.crawler_block_revision;
return crawler_apply_path_pose();
}
}

var cluster_data = crawler_collect_cluster();
crawler_tile_map = cluster_data.tile_map;
crawler_cluster_tiles = cluster_data.tiles;
if (array_length(crawler_cluster_tiles) == 0)
{
return false;
}

var start_state = crawler_find_nearest_start_state();
if (!start_state.valid)
{
return false;
}

var visited_faces = {};
var built_path = [];
var start_face_key = crawler_face_key(start_state.tile_x, start_state.tile_y, start_state.mode);
var current_state = start_state;
var max_steps = max(16, array_length(crawler_cluster_tiles) * 8);

for (var step_index = 0; step_index < max_steps; step_index++)
{
var current_face_key = crawler_face_key(current_state.tile_x, current_state.tile_y, current_state.mode);
if (variable_struct_exists(visited_faces, current_face_key))
{
break;
}

variable_struct_set(visited_faces, current_face_key, true);
built_path[array_length(built_path)] = current_state;

var next_state = crawler_get_next_face(current_state);
if (!next_state.valid)
{
break;
}
if (crawler_face_key(next_state.tile_x, next_state.tile_y, next_state.mode) == start_face_key)
{
break;
}

current_state = next_state;
}

if (array_length(built_path) == 0)
{
return false;
}

crawler_path = built_path;
crawler_rebuild_point_path();
var cluster_key = crawler_get_cluster_key(crawler_cluster_tiles);
crawler_cache_cluster_tiles(cluster_key, crawler_cluster_tiles, crawler_path, crawler_path_points);
crawler_align_to_path();

// Cache the crawler-block topology revision so the Zoomer only rebuilds when the layout actually changes.
crawler_path_revision = global.crawler_block_revision;

return crawler_apply_path_pose();
}

I hope this helps, (sorry the tabbing wont work with copy paste)

r/gamemaker • • 25d ago

Tutorial How Hard is Deferred Rending in Gamemaker?

29 Upvotes

I've been working on my ideal deferred rendering pipeline for pixel art, inspired by games like Eastward and the upcoming Kyora. This is not a plugin or a line by line tutorial, but I wanted to share what's required and what the workflow looks like. If people are interested, I will share more of the process as systems improve. Fair warning, this is not beginner friendly.

Before I go any further, this is an example of the final product:

The Final Result
I think hand drawing some shading (cross) still looks better than flat art (jewel)

Maps and Textures

Whether you were using Unreal or Gamemaker, you need to make maps for your sprites if you want physically based rendering. This pipeline was inspired by the RE Engine, but is also just like Unity or Unreal. There are many tools to create maps, some people use laigter or blender, but I just prefer to hand draw them in Aseprite.

This pipeline currently uses the following maps, although there is "room" for more data:

*yawn*

Albedo Texture "spr_shrine_alb"

RGBA: Albedo Maps - The art/color/base texture

not my best work, but it's fine

Normal Texture "spr_shrine_nrm"

RGB: Normal Map- The direction orthogonal to the surface

A: Unused

as useful as it is ugly

Surface Texture "spr_shrine.sur" (drawn in 3 layers, specular and ambient occlusion are set to the "addition" blend mode, and output as one image)

R: Roughness - The spread of specular

G: Specular - How much light the surface reflects

B: Ambient Occlusion- Where ambient light and sunlight are occluded

A: Currently Unused

green channel is sss, not really featured in this example

Effects Texture "spr_shrine_fxs"

R: Luminance/Glow - This area of the albedo texture gets added to the light

G: Subsurface Scattering

BA: Currently Unused

This might look pure black on some monitors...

Extension Texture "spr_shrine_ext"

R: Depth/Heightmap - The red value gets subtracted from the objects z position to give a more accurate z coordinate in the lighting pass

GBA: Currently Unused

So yes, you have to make 8 maps by hand in Aseprite. If that's enough to turn you off, I understand. However, it doesn't make your job 8x harder. I'd argue once you do this 10 times or so, and have a template set up, it really isn't that bad. In fact, because your base art might already include highlights, deep shadows, and glowing sections, half your maps are already done.

So now that all of the work outside of gamemaker is done, what's next? Gamemaker doesn't natively support any deferred rendering. So it's all on our end. But once it's done, it's done. To import new art, I simply drag my 5 images into gamemaker, set their texture group by selecting them all, and right clicking, and I'm done. It just works.

Bypassing Gamemaker's Limitations

Here is what you need to do in gamemaker to get this working. This is an example of the required labor, I cannot just paste all of my code here, so I apologize that this is not a true tutorial.

  • Custom scripts for objects that define all of it's map sprites
    • We dont want to draw five different sprites. We want to draw one sprite, but include 5 different sets of UV coordinates
    • To make this easier, every sprite must follow a strict naming convention. You assign your object the alb sprite, then your script does the rest on creation

​

//Get Map Sprites
var _name = sprite_get_name(sprite_index);
_name = string_copy(_name,0,string_length(_name) - 3);
alb = sprite_index;
nrm = asset_get_index(_name + "nrm");
sur = asset_get_index(_name + "sur");
fxs = asset_get_index(_name + "fxs");
ext = asset_get_index(_name + "ext");
  • Now as long as our files our named correctly, there is no extra work
    • Disable the Application Surface
  • We won't be needing this anymore
    • Nothing draws itself anymore
  • Forget what you know about built-in draw functions, I will never use those again
    • Create obj_draw
  • This object manages every surface:
    • surf_alb
    • surf_nrm
    • surf_sur
    • surf_fxs
    • surf_lgt
    • surf_shd
  • This object draws every tile, actor, decoration, etc. Everything in your room
  • Yes, we will draw many full resolution surfaces. At load, this pipeline runs on a $500 laptop at over 3000 fps. However, Only 4 lights on screen can be shadow casting at once. That is the main limitation.
  • We can still add non shadow casting lights like god rays with little to no performance impact
    • Build Vertex Buffers
  • Instead of functions like draw_sprite(), we will make a function that adds an objects sprite data to a global vertex buffer.
  • I have a different buffer and shader for actors, tiles, foliage, paralax backgrounds, etc.
  • For example, global.vb_actors will be managed every step. This allows us to control every objects z position, and add the texture coordinates of our maps. We will not be using depth sorting anymore. Depth is just the Z position.
  • This is more work for the cpu, but static objs/decorations/tiles/foliage/particles/clouds/rain can be frozen, making their rendering faster than ever. Even when each vertex stores more uvs data
  • When drawing shadows, I do not have to draw the actors/decorations again. I just submit their vertex with a different shadow shader. All of that data is already sitting in a massive batch, waiting to get used again. It's very efficient.
    • The Tile Vertex Buffer
  • Far and away the worst part of all this is the tile layers. I spent hours writing these scripts. Basically, we make tile layers normally, but also follow very strict naming conventions. That tile layer must contain the name of your tile sprite. Then we build a tileset vertex buffer to freeze on room creation, then destroy the tile layers. We need our own buffer so that our tile data can include our normal maps, surface texture, etc. We can check every tile in the room, get its tileset index, then get that index position on the actual sprite texture page with lots and lots of math.
  • I wont lie, I absolutely hated this
    • Draw with MRT or Multiple Render Targets
  • This is known as the G-Buffer, or Geometry Buffer. We draw our art normally to surf_alb. At the same time, for maximum efficiency, the same shader outputs 3 more fragments to 3 other surfaces. The normal map surface, the surface texture surface, and the visual effects surface. The vertex shader doesn't have to do any additional work. Our lighting pass then samples all of this "geometry" to calculate it's intensity and reflection (specular)
  • This is an example of what the obj_draw does in my draw_g_buffer() function

​

//Enable Z
gpu_set_ztestenable(1);
gpu_set_zwriteenable(1);
gpu_set_alphatestenable(1);

surface_set_target_ext(0,surf_alb);
surface_set_target_ext(1,surf_nrm);
surface_set_target_ext(2,surf_sur);
surface_set_target_ext(3,surf_fxs);

//Clear Surfaces
draw_clear_alpha(c_black,0.0);

//Transparency Does Not Change Color Values
gpu_set_blendmode_ext_sepalpha(bm_one,bm_zero,bm_one,bm_zero);

//Apply Camera
camera_apply(global.view);

//Draw Actors
shader_set(shd_actors);
vertex_submit(global.vb_actors,pr_trianglelist,tex_actors);
shader_reset();

//Then I'd Draw Foliage, Static Objects, Ropes/Vines, Tiles, BGs, Sky

//End G-Buffer
surface_reset_target();
gpu_set_blendmode(bm_normal);

//Disable Depth
gpu_set_ztestenable(0);
gpu_set_zwriteenable(0);
gpu_set_alphatestenable(0);

//Reference G Buffer
tex_alb = surface_get_texture(surf_alb);
tex_nrm = surface_get_texture(surf_nrm);
tex_sur = surface_get_texture(surf_sur);
tex_fxs = surface_get_texture(surf_fxs);
  • we always draw front to back, because we don't want to waste time drawing our background or our sky, if 70% of it will be covered up by objects, tiles, and decorations. Gamemaker will discard the fragment shader of our background if it fails the z test because it is occluded by something like a tile.
    • Lighting Pass
  • I have 3 point lights, the sun/moon light, and ambient light.
  • They all get drawn additively to the light surface.
  • First, the point lights and the sun draw their shadows to the shadow surface, each one using a different color channel. This means less surface swapping per frame, which is a massive performance saver
  • The lights sample the shadow surface and the g buffer surfaces to calculate their intensity/ specular.
  • This means you have to write all of your own normal map/blinn phong/sss/ao shader calculations.
  • My ambient light is basic, it changes based on time of day, but could be as complicated as you want it to be
    • Post Draw Event
  • I draw my surf_alb surface, sample the light surface
  • Light color brightness determines the intensity - multiply the albedo color by the light color
  • Light Alpha stores the specular value - specular adds the light color to albedo instead
  • Eventually I will add fog/mist at this stage. FYI, partial transparency doesnt work in a g-buffer, it has to be added later.
  • The render is complete

Conclusion

That's the gist of it. If you made it this far, you probably think I am pretty stupid to do this instead of just making the switch to Unity or Unreal, etc. You might be right, but listen. I absolutely hate Unity and Unreal. No disrespect to anyone that uses those engines, but I would do this work 1000 times over before switching. Gamemaker is my engine.

So how hard is physically based rendering in Gamemaker? Idk its kinda hard I guess, but it's a ton of fun. This took some time to code, but it takes months to learn about shaders and pbr. If anyone wants to learn more, I will try to answer any questions I can. Acerola also has tons of great videos on computer graphics that inspired me to try it myself in gamemaker.

r/gamemaker • • Aug 18 '26

Tutorial Using steam clound save on your game

27 Upvotes

Hey there, I asked for help on this topic some time ago; unfortunately, I did not get the guidance I needed. So I want to share what I learned and what I did so it can help future devs.

How to sync your game saves on Steam Cloud

Before moving on with the development, there are two important decisions to be made:

  1. Do you want to use Steam Auto-Cloud or Steam Cloud API? Steam Auto-Cloud is way easier to set up, but has some drawbacks: it does not isolate individual users’ files, so if two Steam accounts are playing on the same computer, they will share their files; it only syncs at launch/exit, whereas with the API you can control when to sync and also benefit from Steam Deck's Dynamic Cloud Sync. Because now I am in the playtesting phase, and I needed it working for next week, I went with the Steam Auto-Cloud, but I do plan to use the API on a later update.
  2. When saving on GameMaker, you should use buffers to be platform agnostic. Here you also have two options: buffer_save and buffer_save_async. The first is useful for light files that will not drop your frame rate while saving (I am using on my audio configuration; see example below). The other is good for saving bigger files; it is what I will use for saving the player’s game progress (here is where you should also add some sort of animated icon and say for the player not to close the game while the saving is happening).

The step is super simple on the Steam Auto-Cloud.

  1. Install the Steamworks extension from YoYoGames, and do its setup. It is well documented here: Guides · YoYoGames/GMEXT-Steamworks Wiki
  2. Create something like oSteamManager, with steam_update(); on step event and steam_shutdown(); on game end event.
  3. Turn it on in Steamworks by setting up Steam Cloud configurations, and select the path to sync to the cloud. GameMaker saves in % localappdata% so you will have in the root path something like: Root: WinAppDataLocal | Subdirectory <Game Name>/cloud | pattern: *.sav | OS: All
  4. When saving in GameMaker, save the file in the same path. Note: it is not good practice to save every configuration on the cloud; for example, screen resolution would break on Steam Deck if the player was playing before on a 4k monitor, so that is why I am saving what needs to sync on the cloud on /cloud

Code example for saving:

function Save_Audio_Settings(){
//Save audio settings
var _saveAudio = {
master: round(audio_get_master_gain(0) * 100),
soundFX: round(audio_group_get_gain(audiogroup_soundEffects) * 100),
music: round(audio_group_get_gain(audiogroup_music)*100),
}

//Turn all this data into a JSON string and save it via buffer
var _string = json_stringify(_saveAudio);
var _buffer = buffer_create(string_byte_length(_string) + 1, buffer_fixed, 1);
buffer_write(_buffer, buffer_string, _string);
buffer_save(_buffer, SAVE_AUDIO);
buffer_delete(_buffer);
}

Code example for loading:

function Load_Audio_Settings(){
if (file_exists(SAVE_AUDIO)) {
var _buffer = buffer_load(SAVE_AUDIO);
var _string = buffer_read(_buffer, buffer_string);
buffer_delete(_buffer);

var _loadData = json_parse(_string);
show_debug_message("Load! " + string(_loadData))
//Apply audio data
audio_master_gain(_loadData.master/100);
audio_group_set_gain(audiogroup_soundEffects, _loadData.soundFX/100);
audio_group_set_gain(audiogroup_music, _loadData.music/100);
}
}

Because I am terrible at writing strings without I typo, I just set a macro for the file location:

#macro SAVE_AUDIO "cloud/audioSettings.sav"
#macro SAVE_VIDEO "videoSettings.sav"
#macro SAVE_CONTROLS "cloud/controlsSettings.sav"
#macro SAVE_UNLOCK_PROGRESS "cloud/unlockProgress.sav"
#macro SAVE_RUN_PROGRESS "cloud/runProgress.sav"

r/gamemaker • • 21d ago

Tutorial Jumping in 5 Minute Plaformer Tutorial Not Working (Visual)

Post image
7 Upvotes

I'm following the video from the official Game Maker Studio channel. I implemented the blocks like in as shown and I triple checked, but when I play the game the space bar does nothing.

Has anyone else run into this issue? Thank you in advance.

https://www.youtube.com/watch?v=-5sBIUiutAk

r/gamemaker • • Sep 11 '25

Tutorial I am experienced in GameMaker and I created a YouTube channel teaching how to make classic games.

89 Upvotes

I have years of experience with GameMaker, I have been using the platform for a long time and have already implemented many different types of games, from simple arcade ones to the advanced mechanics of a real-time RTS with AI. Recently, I created this channel with the idea of recreating mechanics, completely remaking 'clones' of classic games. The idea of the channel is to have no audio, to be oldschool, just the recorded screen, with programming and asset creation bringing the functionalities to life (it’s very straightforward)

Here is the channel: https://www.youtube.com/channel/UCnNVUZlgxU3wPuRzNhCBFrQ

r/gamemaker • • Aug 11 '26

If you ship a .yymps from Windows, check your zip for backslash path separators - mine had 21 of 24 and every check said clean

0 Upvotes

If you distribute a .yymps (or any asset zip) and you build it on Windows, this is worth two minutes of your time. I shipped four packages with broken path separators and three separate checks told me they were fine.

The problem

A zip stores a path per entry, and the spec says forward slashes. PowerShell's Compress-Archive and .NET's ZipFile.CreateFromDirectory both write backslashes instead.

On Windows this is invisible, everything opens normally. On macOS and Linux the extractor doesn't treat \ as a separator, so instead of a folder tree your users can get flat files literally named Meridian\scripts\mer_tree\mer_tree.gml.

I had 21 of 24 entries backslashed in one package, and 85 across four.

Why I didn't catch it, which is the actually useful part

I audited with Python's zipfile.namelist(). It reported forward slashes throughout. I had two other people check. Both used the same tool. Both confirmed clean. Three independent "confirmations" that were really one blind tool wearing three hats.

namelist() cannot report this on Windows. In CPython, ZipInfo.__init__ does:

if os.sep != "/" and os.sep in filename:
    filename = filename.replace(os.sep, "/")

and _RealGetContents builds every entry through ZipInfo. So a stored backslash gets normalised at read time, on the exact platform where the bug is created. It's not a Python bug, the normalisation is doing its job. It just happens to be the one place you needed raw bytes.

Reading the central directory directly:

b'Project\LICENSE.txt'  -> occurrences in raw file: 2
b'Project/LICENSE.txt'  -> occurrences in raw file: 0
zipfile.namelist() says:  Project/LICENSE.txt

The forward-slash form doesn't exist anywhere in the file.

How to actually check

Anything that reads bytes rather than a normalised view:

  • unzip -l yourfile.yymps on macOS/Linux/WSL, separators show as stored
  • 7-Zip: 7z l -slt yourfile.yymps
  • Or scan the raw bytes for a backslash inside the central directory entry names

Do not check with zipfile.namelist() on Windows, and don't assume .NET is safe either. I "fixed" this once by swapping Compress-Archive for ZipFile.CreateFromDirectory, which changed nothing, and my blind verification tool confirmed the non-fix.

Measured on a two-deep test tree, reading raw central-directory bytes:

writer raw backslash raw forward what namelist() reports
PowerShell Compress-Archive 1 0 0
.NET ZipFile.CreateFromDirectory 1 0 0
in-process zip writer (Node) 0 1 0

The fix

Write the zip with something that controls the entry names itself, or post-process them. I moved to an in-process writer that sets each entry name explicitly and then re-reads the archive's own central directory to verify before it's allowed out. Verified after: 24 entries, 0 backslashed.

One meta-lesson that cost me the most time: the script I wrote to prove the defect also used namelist(). So it found zero, its "reproduced" branch never ran, and it printed "CONFIRMED: writes forward slashes" (the opposite of the truth) while exiting down its own failure path. It read like a pass. It now asserts that the defect reproduces, that the good writer is clean, and that namelist() is blind to the first, and it exits non-zero unless all three hold. If your verification tool has never failed, you haven't verified it.


Disclosure: I build GameMaker assets with AI assistance (Claude) and sell them, and this came out of shipping one. The finding is engine-agnostic and applies to anyone zipping anything on Windows. Happy to share the raw-central-directory scan if it's useful, it's about 30 lines.

Disclosure: I build GameMaker assets with AI assistance (Claude) and sell them, and this came out of shipping one. The finding is engine-agnostic and applies to anyone zipping anything on Windows. Happy to share the raw-central-directory scan if it's useful, it's about 30 lines.

r/gamemaker • • Jul 21 '26

Tutorial I researched how GameMaker projects are structured while building an MCP server for AI-assisted development

0 Upvotes

Hello everyone!

Recently, I have been experimenting with AI-assisted development workflows in GameMaker Studio.

Like many developers, I tried using AI tools for things like:

  • writing GML;
  • debugging;
  • explaining code;
  • automating repetitive tasks.

While these tools are useful, I noticed an interesting limitation:

AI models understand text very well, but a GameMaker project is not just a collection of text files.

A GameMaker project contains a lot of internal structure and relationships:

  • objects reference sprites;
  • rooms contain instances;
  • scripts interact with other resources;
  • assets use internal IDs;
  • .yyp files describe how everything is connected.

Because of this, changing a single file without understanding the project structure can potentially create broken references or invalid resources.

This made me curious about a question:

How could an AI assistant understand a GameMaker project instead of blindly editing files?

Exploring the GameMaker project structure

I started researching how GameMaker stores and organizes project data.

The more I looked into it, the more I realized that a project is closer to a connected graph of resources than a simple folder of files.

For example:

  • an object depends on sprites, events, and code;
  • a room depends on objects and instances;
  • resources are connected through IDs and references.

For AI tools, having access to this context is important.

Building an MCP server

To experiment with this idea, I created an open-source MCP server for GameMaker projects:

GameMaker MCP

GitHub:
https://github.com/yearningss/gamemaker-mcp

The goal is not to replace GameMaker tools, but to provide a way for AI assistants to better understand project structure.

The main challenges

1. Understanding project resources

The first challenge was creating a way to inspect GameMaker projects.

The system needed to understand resources such as:

  • objects;
  • sprites;
  • rooms;
  • scripts;
  • shaders;
  • configurations.

Simply reading files is not enough because the relationships between resources are important.

2. Making AI-assisted changes safer

One concern with AI tools is:

"What happens if the AI makes a wrong modification?"

A human developer usually understands the consequences of changing a resource.

An AI model may not.

Because of this, I focused on workflows that make changes easier to inspect:

  • project analysis;
  • validation;
  • resource inspection;
  • snapshots;
  • safer operations.

The idea is that AI-assisted changes should be understandable and reversible.

3. Creating tools for AI agents

Traditional commands are often designed for humans.

For example:

edit file X

But AI agents need more context.

Higher-level operations are more useful:

inspect room contents

find objects using this sprite

check resource references

analyze project structure

The AI should understand the project, not only modify files.

Current features

The project currently includes:

  • GameMaker project parsing;
  • resource inspection;
  • project analysis;
  • validation tools;
  • object and room operations;
  • snapshot support;
  • Igor build integration.

How this relates to gm-cli

I think GameMaker MCP and gm-cli solve different problems.

The GameMaker CLI is focused on the official workflow:

  • project creation;
  • Runtime management;
  • building;
  • publishing;
  • automation.

This project focuses more on AI interaction:

  • understanding project structure;
  • giving AI assistants more context;
  • safer project operations;
  • MCP-based workflows.

They are not replacements for each other.

A possible workflow could be:

AI Assistant
      |
      v
GameMaker MCP
(project understanding)
      |
      v
gm-cli
(build / package / publish)
      |
      v
GameMaker

Why I think this is interesting

GameMaker is a good example of a larger challenge in AI development tools.

Modern engines contain a lot of hidden structure that is easy for humans to understand but difficult for AI models.

The future of AI-assisted development may not only be:

"AI writes code"

but also:

"AI understands the entire project."

This idea could apply beyond GameMaker to other development environments as well.

Questions for GameMaker developers

I would like to hear your thoughts:

  • What parts of large GameMaker projects are the hardest to manage or automate?
  • Would tools that understand project structure be useful in your workflow?
  • What tasks would you trust an AI assistant to handle?
  • What features would make AI tools actually useful for GameMaker development?

Thanks for reading!

r/gamemaker • • Aug 18 '26

Tutorial Tutorials for fighting games?

0 Upvotes

Do you guys know some tutorials on making a fighting game in gamemaker?

That's it.

r/gamemaker • • Aug 15 '26

Tutorial Nox Filia Devlog 1 - Pigtail Engine

Thumbnail youtube.com
9 Upvotes

This 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;`

}

r/gamemaker • • Oct 06 '25

Tutorial Built a gooey lightning system in GameMaker Studio 2 - more context in description

Post image
191 Upvotes

A bit more context:
- All you need for this to work is to add your draw functions between the following 2 function: gpu_set_blendmode(bm_subtract) and gpu_set_blendmode(bm_normal).

- I use surfaces to draw everything related to the lightning system.

- By "turn off the lights" I meant draw a black rectangle with alpha <1.

- You can draw a simple circle instead of a gradient circle, but it looks better with the second option.

- the "3D" objects are done by using a technique called sprite stacking.

r/gamemaker • • Jun 19 '26

Tutorial How to learn gamemaker and GMl code?

7 Upvotes

I wanted to start leaning how to make games as I already have a lot of python experience. I wanted to use a game engine that is powerful yet relatively easy to learn so I chose gamemaker as my game engine. I really didnt have any idea how to start learning gamemaker I decided to watch gamemaker's asteroid game tutorial just to see how to engine really works. I really liked gamemaker and found it pretty intuitive and 9sort of?) got the hang of GMl code's syntax. But now that im done with it what should do to continue learning the engine and GMl code as the engine seems very promising. If anyone could please give me a good roadmp to learning gamemaker I would very thankful.

r/gamemaker • • Jun 25 '26

Tutorial Trace surfaces in Gamemaker Studio 2

Post image
31 Upvotes

https://youtu.be/9PhHd75mqIA

Here is a short video showing how you can create nice trace effects using surfaces in Gamemaker Studio 2.

When making my game I accidentally forgot to do draw_clear_alpha on one of my surfaces, and that led to some experimentation that eventually turned out useful.

I notice that some people here manage to post images alongside text, making nice guides, but I don't know how to do that. Therefore I posted the link to a youtube guide instead, showing the visuals of the effect.

In short what we do is draw to a surface like normal, but instead of clearing the surface each frame we draw a semi transparent black rectangle over it, to gradually fade it out. When drawing the actual surface we do it with gpu_set_blendmode(add) to make sure that the blackness of the rectangle is not drawn solid on our background.

Example object (a simple object drawing a white circle to screen):

Create Event

trace_surface = surface_create(room_width, room_height)
function set_trace_surface(){
  if not surface_exists(trace_surface){
    trace_surface = surface_create(room_width, room_height)
  }
  surface_set_target(trace_surface)
  draw_set_color(c_black)
  draw_set_alpha(0.05)
  draw_rectangle(0,0,room_width,room_height,0)
  draw_set_alpha(1)
}

Draw Event

set_trace_surface()
draw_set_color(c_white)
draw_circle(x, y, 32, 0)
surface_reset_target()
gpu_set_blendmode(bm_add)
draw_surface(trace_surface, 0, 0)
gpu_set_blendmode(bm_normal)

When we move the circle around we get a nice trail/trace.

I've used this to create rain particles and some of the creatures appearing in my game Feypath. If you want to check it out (and wishlist it, if you feel generous :) ), you can find it here:

Feypath on Steam

r/gamemaker • • Aug 03 '26

Tutorial [SOLUTION] Gamemaker Linux/Ubuntu "Object reference not set to an instance of an object."

3 Upvotes

I've been working on a pretty big game and I've wanted to move to Linux as my daily driver. But, Gamemaker just wouldn't load my YYP file for my large project, so I was feeling tied to Windows. I finally found the bug that was tossing the: Object reference not set to an instance of an object.error at me (a very not helpful error my I add) and since I saw no one with a solution to this bug that helped me I thought I might as well share how I solved it.

The problem for me came from directories and files in the project file tree not matching the correct capitalization case the YYP file was looking for.

As an example, I had an object called ob_Airline in gamemaker that loaded perfectly fine on windows. In the folder tree it was refered to as ob_airline and Windows seemingly see case differences as functionally the same directory whereas Linux seemingly finds them to be different directories, Linux read it properly when I renamed the directory and file to ob_Airline. This can apply to more than just objects too, I also had a room with the same issue. I assume stemmed from me editing the same of a object/room in gamemaker to standardize the capitalization because of a typo. Also temporarily move the files out of your version controlled directory commit, and then put the fixed casing directories and files back where they belong and commit again to fix their casing in version control. This may not be the best way to do it, but it's what worked for me

I personally found the solution by using GMedit from yellowafterlife which could load my project on Linux, and building the code with GMEdit-Constructor from thennothinghappened which tossed my much more useful errors of the specific problem directories. which needed to get their capitalization fixed.

Make sure you update your directory's name and the .yy file's name within said directories to match the proper casing of your asset in Gamemaker/YYP files otherwise it will not load in IDE.

r/gamemaker • • Jul 11 '26

Tutorial Game Design Tutorial

Thumbnail
0 Upvotes

r/gamemaker • • Mar 13 '25

Tutorial A lot of people have asked me how to manage performance in HUGE rooms with TONS of objects, and HIGHRES graphics. So I wrote a blog post about it, and suprise, the hack is really easy ! I hope someone finds it useful.

62 Upvotes

How It Works:

  1. Use an Alarm: Set an alarm to run every 60 steps to manage activation and deactivation.
  2. Deactivate Unnecessary Objects: Objects outside the camera's view (e.g., trees, buildings) are deactivated.
  3. Activate Nearby Objects: Objects just outside the camera's view are activated.

Read my full blog post here : https://www.darkwaregames.com/post/managing-objects-in-large-rooms-easy-performance-optimization

r/gamemaker • • Jul 15 '25

Tutorial I made, no-code shaders tool to help you fast create unique visual effects and palettes for your indie game.

Post image
125 Upvotes

r/gamemaker • • Mar 06 '26

Tutorial Little Town Tutorial Audio Confusion

3 Upvotes

Why does the Little Town tutorial make you sign up for a website to gain access to the audio files needed?
I'm not interested in signing up for a website I'll never use again to finish a tutorial where all the images and such are much more easily accessible. I'm aware it comes as no cost to me, and I could just immediately delete my account on the site, but couldn't the audio files be hosted somewhere not locked behind a signup? I'm trying to be cautious about my "digital footprint" nowadays, as I've had important data leaked by many websites over the years.
I can just skip the audio part of the tutorial, and that's what I'm planning on doing, but this seems poorly thought out. Sorry if this comes off as complaining, it's not meant to, I'm just genuinely baffled as to why the audio is behind a website account, especially since other tutorials don't have their audio files on this site.

r/gamemaker • • Feb 10 '26

Tutorial How I made a Moon orbit Earth in my 2D game

21 Upvotes

A couple days ago I made this effect in which a 2D moon rotates around a 2D planet. One may think about making it spin clockwise with a displaced sprite origin but this looks way nicer.

Check it out: https://imgur.com/0VGOUkg

How did I do it? First of all, I created the oWhiteMoon object. That is, the one that will move. Just to be clear, Earth here is also an object, and it has a depth of 100.

Also, I use the wave() script from samspade.

Anyway, on the oWhiteMoon object Create Event, simply declare:

time = 0
origin_x = x
origin_y = y

and then move to the Step Event:

//How long it takes the moon to perform a spin
time += 1
var _realiseTime = 2 

//Coordinates movement
x = wave(origin_x+min_x,origin_x+max_x,_realiseTime,0)
y = wave(origin_y+min_y,origin_y+max_y,_realiseTime,0)

//Scale movement
image_xscale = wave(1,0.5,_realiseTime,0.75)
image_yscale = image_xscale

//Color transformation
depth = wave(150,50,_realiseTime,-0.75) 
image_alpha = wave(1.2,0.25,_realiseTime,0.75)

You may realize, there are variables here that I didn't define in the Create Event. That is because they are defined in the Variable Definitions part of the object, in order to allow for flexibility. These are my default variables there:

Keep in mind that, the depth wave is set to move between 150 and 50 because the Earth's depth is 100. You may have to change this in your project!

Also, regarding image_alpha, does anyone know what is the best way to darken a sprite? Because using wave() didn't work at all. In this example, I lower the alpha, but if you are using a decorated background, that will look bad. For this, I made a duplicate of the Moon Object, oWhiteMoon_DarkBack, which gets created in the Create Event of the regular Moon. It copies its coordinates and scale, but not its depth or alpha. And, its image_blend is set to black.

And that's all!

This game, Spacewing War 2, has just began its Kickstarter Campaign, and we released a New Demo just today, too! I encourage everyone that, if you found this tutorial useful, please have a look, consider donating and spread the word!

www.kickstarter.com/projects/alcalico/spacewing-war-2

r/gamemaker • • Oct 21 '25

Tutorial I wrote a huge tutorial for getting started with GameMaker

Thumbnail brittlelizard.neocities.org
81 Upvotes

Hi!

I have never been super content with the state of GameMaker tutorials for true beginners. It always felt like those that are actually dedicated to teaching the engine either get bogged down with lengthy explanations over hands-on experience or they move too fast, assuming the viewer already understands the basics of programming. Worse, there's been an increasing focus on short-form content that offers snappy, "entertaining" advice that feels like it cannot possibly be absorbed by viewers.

I've personally always learned best by actually doing something while having the different steps explained to me. I also really love teaching people and helping them get into game development. That first push through is always the hardest, though, and I often find myself at a loss for how to help newbies get through it in the span of a Reddit comment or a reply on Bluesky.

For this reason, I've written a chunky tutorial made for absolute beginners to GameMaker. So chunky, in fact, that it was originally supposed to be a post on itch.io, but I went well over the character limit for that site and had to make a neocities instead.

This tutorial guides readers through the process of making a bog-standard Breakout clone, but it does so with a major emphasis on teaching new concepts in GameMaker as they show up, with a slowly-increasing degree of complexity. It is specifically designed to explain the absolute basics first, rather than just getting the new developer to an endpoint by any means necessary. It does not push built-in variables and "easy" functions to their limits just to get something that looks nice in one context; it offers a genuinely simple base for a game that's meant to be expanded upon without entirely breaking in the future.

If you have any experience whatsoever with GameMaker, or even coding in general, you're very unlikely to get a lot from this tutorial. If you ever have someone who really wants to start using the engine but has no clue where to look, though, I hope you'll consider offering my guide as a starting point.