Quick start guide - Homunculus84/Binder GitHub Wiki

Welcome to Binder, a binary search library for Game Maker Studio 2.3+.

This guide will walk you through setting up a Binder instance, indexing your data, and performing a basic search.

Installation

Import the scripts into your project by downloading the latest release in .yymps format and drop it into your project.

Configuration

Find the binder_config script in the Binder folder.

Constant Type Description
BINDER_SHOW_WARN Boolean Whether to output warning messages to the console (true) or not (false)

Creating your first Binders

You can think of a Binder as transformed & sorted version of your source data array, with the goal to perform binary searches on it (but no worries, Binder will not straight up duplicate nor alter your original data in any way). You can keep as many Binders as you want for the same data.

We'll use as an example a dictionary of words (imagine a word game), where each word is worth some points.

We set 2 goals:

  • Search for a word
  • Search for all words with a specific length (number of characters)
dictionary = [
    { word: "apple",   points: 2 },
    { word: "tower",    points: 2 },
    { word: "trash",  points: 5 },
    { word: "ice",     points: 6 },
    { word: "cloud",     points: 3 },
    { word: "training",     points: 5 },
];

// Create a Binder indexed by word (alphabetically)
word_binder = new Binder(dictionary, function(_entry) { return _entry.word; });
word_binder.build_index();

// Create a Binder indexed by word length
word_length_binder = new Binder(dictionary, function(_entry) { return string_length(_entry.word); });
word_length_binder.build_index();

Hint: You can also pass true as third argument when creating a binder to generate the index directly, instead of calling build_index()

Searching

To perform a binary search on a Binder, you can either use the find() or search() methods. The former looks for (and returns) a single value, while the latter looks for multiple values, and returns a BinderResult.

Basic lookups

// Look for the word "apple"
var _result = word_binder.find("apple");
if(!is_undefined(_result)) {
    show_debug_message($"Found: {_result}");
}
// Look for all words having 5 letters
var _result = word_length_binder.search(5);

Mapping the lookup value

Sometimes it is useful to apply the same function you used to index your binder to your search value.

In the case of the word_length_binder above, we created the index by applying string_length(_entry.word). By passing true as second argument to the search() (or find()) function, we are telling we want the same logic applied to the search value.

This means we can easily perform a search like "find all the entries that have the same word length as this other entry in the dictionary".

var _other_entry = { word: "rat",   points: 8 };
var _result = word_length_binder.search(_other_entry, true); // <- Note the second argument

The above produces the same result as

var _result = word_length_binder.search(string_length(_other_entry.word));

Advanced applications

A binary search is not limited to finding exact matches. For example, we can leverage the alphabetical order of word_binder to look for prefixes (words starting with...).

var _prefix = "to";

// Search for words starting with "to"
var _result = word_binder.search(_prefix, false, method({_prefix}, function(_word) {
    return string_copy(_word, 1, string_length(_prefix));
}));

Note: Don't be confused by the method() call, it's just a way to pass _prefix to the function, since game maker doesn't support closures.

Working with search results

The find() method, being limited to a single result, returns undefined (if not found) or the entry directly.

Calling search() instead always returns a BinderResult struct (even with no results). You can use this struct variables and methods to get your data in different ways.

Getting the found value(s)

// Find all 5 letter words
var _result = word_length_binder.search(5);

// Check if we got any result
if(_result.count > 0) {
    // Fetch the first 10 entries (or less, if count < 10)
    var _entries = _result.fetch_values(10);

    // Print all the found entries along with their score
    for(var _i = 0; _i < array_length(_entries); _i++) {
        var _entry = _entries[_i];
        show_debug_message($"{_entry.word}: {_entry.points}");
    }
}
else {
    show_debug_message("No words found!");
}

Hint: Binder also provides some helper functions to merge the results of different searches on the same binder: binder_results_intersection() and binder_results_union()

Saving / Loading Binders

Generating Binder indexes is an expensive operation. It may not be problematic with medium / small datasets, but it adds up really quickly on larger ones.

It is strongly advised to ship your game with pre-build Binders, or alternatively, generating them on the first run to be stored in the file system for the next runs.

Load from file or build & save if not found

// Create a Binder ()
binder = new Binder(data, binder_pass_through);

// Synchronously load a Binder index from the file system
var _success = binder.load("binder.json");

// If failed to load, build & save
if(!_success) {
    binder.build();
    binder.save("binder.json");
}

Note: binder_pass_through is an helper function that simply returns its own argument. It's equivalent to function(_v) { return _v; }

You can also provide your own saving and loading mechanism by using binder.to_json() and binder.from_json() to serialize / deserialize the data.

Further reading

Check out the function reference.