MinHeap Priority Queues, an alternative for ds_priority

MinHeap Priority Queues, an alternative for ds_priority

Since GameMaker 2.3, I've started using the ds_ functions less and less, with their old-school memory management requirements and managing numeric indexes, and all the bugs that can come from that.

The alternatives for ds_map is clearly a struct; and ds_grid, ds_stack and ds_queue are also achievable using arrays in a relatively trivial way. But ds_priority is one of the more complex ones to replace since under the hood it's not just a straightforward data structure, it needs a few extra bells and whistles to work.

Long story short, I now just use my own constructor-based replacement for ds_priority that implements a Heap under the hood. Here it is for everyone who wants a more modern priority queue that is GC'd:

GML MinHeap implementation
GML MinHeap implementation. GitHub Gist: instantly share code, notes, and snippets.
GML MaxHeap implementation
GML MaxHeap implementation. GitHub Gist: instantly share code, notes, and snippets.

What is a ds_priority?

A ds_priority is a datastructure that helps keep track of items and priorities, often referred to as a "priority queue". You can insert value-priority pairs into it in any order you want, and at any time you can query for the lowest or highest priority value in the list.

This is useful for things like correctly ordering your UDP netcode packets which arrive out-of-sequence. Or taking a list of characters with different initiative values and figuring out their move order. Or it's the core of graph and path-finding algorithms such as A*.

From a computational-efficiency point of view, it is a "sort-on-insert", meaning all the computational power necessary to make sure things are order are incurred at the moment of inserting the value. In games, this is quite good for situations where you gradually add values to the ds_priority since you spread out the computation power of sorting; so that when you pull values out of it later, you incur less processing power, which means fewer lag spikes. This is in contrast of the opposite approach of inserting into an unsorted array (which is very fast), and then having to sort the array when you need to use it, which is slow and can incur a lag spike when you do it.

The Heap data structure

I won't get into the details of how this works, but suffice to say that a Heap is a way of structuring data in a way where the priority-order is maintained. ds_priority is almost certainly a Heap data structure under the hood. You can find out more about the Heap data structure on Wikipedia:

Heap (data structure) - Wikipedia

MinHeap vs MinMaxHeap

The main difference between my MinHeap implementation and ds_priority (aside from MinHeap being implemented as a constructor and arrays, meaning you don't have to worry about memory management) is that ds_priority is a MinMaxHeap, in that it keeps track of both the minimum and maximum priorities of the heap. You can easily read and remove either the minimum priority item or the maximum priority item at any given time.

My MinHeap and its brother, MaxHeap implementation only does one of these: MinHeap lets you only remove the minimum priority value item from the heap, and doesn't have a way for you to look at the maximum priority. And similarly MaxHeap only lets you remove the maximum priority value item.

It is possible to write a MinMaxHeap implementation, I just haven't done it. I haven't come across a situation where both are needed yet. I've only seen use-cases where you want one or the other. But perhaps this will change, time will tell.

MinHeap Usage Example

Using the code linked at the top of the article, minheap usage looks something like:

turn_order = new MaxHeap();
// insert a bunch of values and their priorities
turn_order.insert(PlayerA, 4);
turn_order.insert(PlayerB, 6);
turn_order.insert(PlayerC, 2);

// Who has the lowest priority?
var _highest_priority_player = turn_order.peek_max_value();

// Process players in turn
while (turn_order.get_length() > 0) {
  var _player = turn_order.pop_max_value();
  process_turn(_player);
}

If you're looking for a more modern way to make a priority queue, use a Heap implementation like this one, and free yourselves from the tyranny of having to memory-manage ds_priority