Skip to content

Instantly share code, notes, and snippets.

@davydmaker
Created July 16, 2025 01:58
Show Gist options
  • Save davydmaker/bc55c720c5247cd7472a40a9da823563 to your computer and use it in GitHub Desktop.
Save davydmaker/bc55c720c5247cd7472a40a9da823563 to your computer and use it in GitHub Desktop.
GameMaker GML: Unique Frame Selector System
/**
* Frame Selector System - Randomly selects frames without repetition
* Ensures each frame is used exactly once before any frame is repeated
*/
/**
* Creates a shuffled frame selector for the current sprite
*
* @return {struct} Frame selector object with methods
*
* @example
* // Initialize frame selector
* frame_selector = create_frame_selector();
*
* // Get next unique frame
* var next_frame = frame_selector.get_next_frame();
* image_index = next_frame;
*/
function create_frame_selector() {
var selector = {};
// Initialize with current sprite's frame count
selector.total_frames = sprite_get_number(sprite_index);
selector.unused_frames = [];
selector.reset_pool = function() {
unused_frames = [];
// Populate array with sequential frame numbers (0, 1, 2, ...)
for (var i = 0; i < total_frames; i++) {
array_push(unused_frames, i);
}
// Shuffle the array for better randomness
shuffle_array(unused_frames);
};
/**
* Gets the next unique frame from the pool
* @return {real} Frame index, or -1 if no frames available
*/
selector.get_next_frame = function() {
// Reset pool if empty (all frames used)
if (array_length(unused_frames) == 0) {
reset_pool();
}
// Safety check
if (array_length(unused_frames) == 0) {
show_debug_message("Warning: No frames available in sprite");
return -1;
}
// Get last frame (already shuffled, so effectively random)
var selected_frame = array_pop(unused_frames);
return selected_frame;
};
/**
* Gets remaining frames count
* @return {real} Number of unused frames
*/
selector.get_remaining_count = function() {
return array_length(unused_frames);
};
/**
* Checks if pool needs reset (all frames used)
* @return {bool} True if pool is empty
*/
selector.is_pool_empty = function() {
return array_length(unused_frames) == 0;
};
// Initialize the pool
selector.reset_pool();
return selector;
}
/**
* Utility function to shuffle an array in-place using Fisher-Yates algorithm
* More efficient and better randomness than multiple random selections
*
* @param {array} arr - Array to shuffle
*/
function shuffle_array(arr) {
var n = array_length(arr);
for (var i = n - 1; i > 0; i--) {
var j = irandom(i);
// Swap elements
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
/**
* Simple standalone function for basic use cases
*
* @param {array} frame_pool - Array of available frame indices
* @return {real} Selected frame index, or -1 if pool is empty
*/
function choose_frame_unique(frame_pool) {
if (array_length(frame_pool) == 0) {
return -1;
}
// Use array_pop for O(1) operation instead of array_delete
return array_pop(frame_pool);
}
// =============================================================================
// USAGE EXAMPLE
// =============================================================================
/*
// In Create Event:
global.frame_selector = create_frame_selector();
// In Step Event or when you need a new frame:
var next_frame = global.frame_selector.get_next_frame();
if (next_frame != -1) {
image_index = next_frame;
show_debug_message("Selected frame: " + string(next_frame));
show_debug_message("Remaining frames: " + string(global.frame_selector.get_remaining_count()));
}
// Alternative simple usage:
// Initialize once
if (!variable_instance_exists(id, "frame_pool")) {
frame_pool = [];
for (var i = 0; i < sprite_get_number(sprite_index); i++) {
array_push(frame_pool, i);
}
shuffle_array(frame_pool);
}
// Use whenever needed
var frame = choose_frame_unique(frame_pool);
if (frame != -1) {
image_index = frame;
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment