Using data structures in your games: Binary and Quad trees/Grid collisions

Overview

  • Although in general most data structures used in programming don’t really have a viable use in game development, I’m going to cover the ones that have helped me in creating useful and efficient solutions in my code.
  • For this first post I want to cover Quad trees mainly, but I’ll briefly go over binary trees to provide context as this is an abstract and somewhat difficult concept to grasp.

Binary trees

The basics

My personal go-to source for learning about data structures and algorithms is https://geeksforgeeks.org. They have a nice bunch of sources on binary trees here. In general, a binary tree can be thought of as a simple node. This node has a left and right child, which also happen to be nodes (and will also have left and right childs, which will also be nodes… :exploding_head:). The concept is difficult to grasp at first, but it is a very useful and efficient way of containing and traversing through data. Think about it this way, if you have 3 items, you’ll need a binary tree with 2 layers to contain it. If you have 43 items, you’ll need 6 layers. And for any x amount of nodes, you’ll be able to store it in math.ceil(lg(x+1)) layers, where lg represents log base 2. So, trees become insanely efficient for large chunks of data. For a million nodes, it would take as many as a million loop iterations to find a specific one if they were stored in an array, but in a binary tree it won’t take more than 20 traversals! For our purposes, we’ll use a special type of a binary tree called a Binary Search Tree (BST), since we want our nodes to be sorted. Note: a traditional binary tree will not have any form of a “remove” function! It is simply a means of storing values and reading them all out in order. If you want to organize your data in such a way that you can always keep it in order and also add/remove elements at your will while maintaining that order, you’ll have to use priority queues, which I might make a tutorial on later.

The functions

For this case I’ll keep it simple and just have functions for inserting nodes and printing out values. When inserting a value, we’ll have to do a bit of traversing the tree to figure out where the node will go. Remember, we’re doing a binary search tree, so for every node we have to maintain the following: given their existence, the left child has to be less than its parent, and the right child has to be greater than its parent. For ordering our tree, a recursive approach will be best suited for our needs. Basically, if a node n has left child a and right child b, it must be true that a<n<b, right? Using this concept and the magic of recursion we’ll easily make a function that prints out our tree in order.

The code

With that, let’s set up the basics of our code:

function NewNode(value, identifier)
    return {Value = value, Identifier = identifier}
end

local BinaryTree = {}

BinaryTree.__index = BinaryTree

function BinaryTree.new(node)
    return setmetatable({Node = node}, BinaryTree)
end

Easy enough. Now we have a simple object that we can instantiate and call functions on. Let’s tackle inserting, as that’s the hardest part:
We want to traverse through the tree until we run into a dead end (i.e. child we’re looking for is null). So, let’s maintain two local variables, current and parent. We loop through the tree until current is nil, and by using the parent variable we can place our node where current should have been.

function BinaryTree:Insert(node)
	local parent = self
	local current = self
	while current do
		parent = current
		if node.Value < current.Node.Value then
			current = current.Left
		elseif node.Value > current.Node.Value then
			current = current.Right
		else
			warn("Insert failed; duplicate node") return
		end
	end
	if node.Value < parent.Node.Value then 
		parent.Left = BinaryTree.new(node)
	else
		parent.Right = BinaryTree.new(node)
	end
end

All we do here is go left if our new value is less than the current node and go right if its greater. Once we hit a dead end we compare the node one last time to determine whether it should be on the left or the right, and create the connection accordingly.
NOTE: There is a much more efficient way to go about this! Instead of adding another level to your code by introducing nodes you could simply store the values and identifiers in the BinaryTree structure itself. The reason I’m going through this redundantly is to better outline the structure that will be used for our QuadTree, where it won’t be as manageable to maintain node properties for every tree as not every tree level might have node information tied to it.
Now that that’s out of the way, I’ll simply paste in the order code as it’s strikingly simple:

function BinaryTree:Order()
	if self.Left then self.Left:Order() end
	print(self.Node.Identifier)
	if self.Right then self.Right:Order() end
end

It might seem too good to be true, but it works!
Here’s some driver code that’ll test out our functionality:

local Tree = BinaryTree.new(NewNode(30, "Adult"))
Tree:Insert(NewNode(16, "Teen"))
Tree:Insert(NewNode(60, "Elder"))
Tree:Insert(NewNode(45, "Old man"))
Tree:Insert(NewNode(130, "(Probably) dead guy"))
Tree:Insert(NewNode(3, "Baby"))
Tree:Insert(NewNode(7, "Kiddo"))
Tree:Order()

--output: Baby Kiddo Teen Adult Old man Elder (Probably) dead guy

Look at that! Even though I just put all of those inputs in a random sloppy order, our BST kept them ordered! Imagine trying to do this with an array, with possibly thousands of values… Anyway, I digress.

That’s all I’ll write for binary trees. They have so much more potential and power all around software, but it’s not that useful in game development. However, now that we have a simple understanding of binary trees, let’s jump into their two dimensional analog: Quad trees!

Quad trees

The basics

As complex as binary trees might seem, they’re only one-dimensional. You can only go left and right. Left and right, however, is not enough for 3D. The good news is we can expand binary trees into 2 and 3 dimensions to handle all sorts of collisions! 3 dimensional trees are referred to as octrees and once again I won’t cover those here but might do so in another topic. For now, let’s look at quadtrees. Here’s a nice visual from wikipedia that explains how we might set up a quadtree:
image
Basically, we set up a huge square that we want to be our world space. Then, we can insert nodes by splitting a square into 4 equal squares in such a way that each quadrant only has 0 or 1 nodes. Note that while this quadtree implementation doesn’t split squares into unit sizes unless new nodes are added, and it continues to infinitely downsize quadrants to accomodate for as many nodes as possible, for our purposes we will maintain a unit size quadrant and quadtrees will only have node values if they meet that size, no higher or lower. Take this example of a dummy map:


For this example, we’ll divide this map into 20x20 units and insert each one as a node, and unlike binary trees we’ll also have a function to locate the closest node to a position. So, we’ll be able to use a quadtree to essentially check grid collisions. This is useful for many things, such as custom streaming systems. In this example, I’ll show how these grid collisions can be used to check when a player enters a new area and display a notification then, similar to what you might see in open world games.

The functions

This’ll be a bit more advanced than the traditional binary tree in that we’ll have both insert and search functions. Along with this we’ll have a simple helper function :Contains() as a safe check to make sure what we’re inserting or searching for is in the bounds of the quadtree. I’ll outline the API for our class so you can get a sense of everything that’ll have to be included:

QuadTree {
	Rect Bounds
	Node node
	QuadTree TopLeft
	QuadTree TopRight
	QuadTree BotLeft
	QuadTree BotRight
}

QuadTree QuadTree(Vector2 point1, Vector2 point2, Node node = nil)	

void QuadTree:Insert(Node node)

Node QuadTree:Locate(Vector2 point)

boolean QuadTree:Contains(Vector2 point)

Notice how each QuadTree can potentially have 4 other child trees. These trees will default to nil in the constructor. A tree will either be a node holder with values or it’ll be split into 4 smaller quadrants; in our cases it’ll never be both. Both our insert and locate functions will then have to rely on recursion; if we find the quadrant we want, we’ll place or return the node there, otherwise we’ll just call the same function on the correct child node. I’ll do less explaining here and in turn I’ll annotate the code so the step by step process is more clear.

The code

First off, basic constructor setup:

local UNIT_SIZE = 20 --The minimum side length of a quad subtree

function NewNode(pos, identifier)
	return {Position = pos, Identifier = identifier}
end

local QuadTree = {}

QuadTree.__index = QuadTree

function QuadTree.new(point1, point2, node)
	return setmetatable(
		{
			Bounds = Rect.new(point1, point2), 
			Node = node 
		}, QuadTree
	)
end

Note the UNIT_SIZE variable; this can be changed to your liking. If you wanted to create a custom 2d collision system, this would be much smaller (<1) and you’d configure this implementation to have a multi-bucket tree (i.e. more than 1 node per quadrant being possible). You’d also have to change up a lot of how the Locate function works. Just something that you can try if you have free time, a lot of cool things can be made with it.
Anyways back on topic, we should have a helper function to see if a point is actually in the bounds of our tree. We don’t want weird results because of bad inputs! In essence, we just check if vector2 values are greater than the minimums and lower than the maximums.

function QuadTree:Contains(point)
	local bounds = self.Bounds
	return bounds.Min.X <= point.X
	and bounds.Min.Y <= point.Y
	and bounds.Max.X >= point.X 
	and bounds.Max.Y >= point.Y
end

So how do we go about inserting? Well, this Contains function is actually similar to a big part of what will go into our recursive function: After doing a sanity check to make sure the node is in the tree’s bounds, we will split up our square into 4 smaller ones. The cool thing about this is that we’ll only have to define one more point. In fact, this is the case for any dimension of tree! Think about it, even with a cube if you placed a dot in the middle and then just connected the dots to center points of the faces, you could split the cube into 8. For our purposes, we’ll split the cube into 4, and use a similar approach to the Contains function to figure our what subdivision the node should go in. If that subdivision isn’t initialized yet, we’ll do so, and add the node!

function QuadTree:Insert(node)
	if not self:Contains(node.Position) then
		warn("Insert failed; node is not in quad bounds")
		return
	end
	
	--We don't want to subdivide the quad any further; this serves as the 'base case' for our recursion
	if self.Bounds.Height <= UNIT_SIZE and self.Bounds.Width <= UNIT_SIZE then
		if self.Node then
			warn("Insert failed; quad unit area is already occupied")
		else
			self.Node = node
			return
		end
	end
	
	--At this point, the node is in our quad bounds but we still have to allocate a unit slot for it
	local centerPoint = (self.Bounds.Min + self.Bounds.Max)/2
	local halfRight = Vector2.new(self.Bounds.Width/2,0)
	local halfDown = Vector2.new(0,self.Bounds.Height/2)
	--If we are on the left..
	if node.Position.X <= centerPoint.X then
		--Check if we are in the top left
		if node.Position.Y <= centerPoint.Y then
			if not self.TopLeft then
				--Subdivide the quad if it hasn't been already
				self.TopLeft = QuadTree.new(self.Bounds.Min, centerPoint)
			end
			--Move into our subdivision and attempt an insert there
			self.TopLeft:Insert(node)
		--We must be in the bottom left at this point
		else
			if not self.BottomLeft then
				self.BottomLeft = QuadTree.new(self.Bounds.Min + halfDown, centerPoint + halfDown)
			end
			self.BottomLeft:Insert(node)
		end
	--Check the right now
	else
		--Top right
		if node.Position.Y <= centerPoint.Y then
			if not self.TopRight then
				self.TopRight = QuadTree.new(self.Bounds.Min + halfRight, centerPoint + halfRight)
			end
			self.TopRight:Insert(node)
		--Bottom right
		else
			if not self.BottomRight then
				self.BottomRight = QuadTree.new(centerPoint, self.Bounds.Max)
			end
			self.BottomRight:Insert(node)
		end
	end
end

Now that we’ve done that, the search function will actually be a piece of cake. By using the same splitting function that we did for Insert, we’ll find the subdivision that the point would be in. If that subdivision exists, all we have to do is return the node tied to it!

function QuadTree:Locate(point)
	if not self:Contains(point) then
		warn("Search failed; point not in quad bounds")
		return
	end
	
	--Recursive base case
	if self.Node then return self.Node end
	
	
	--Recursive step, similar to search
	--However, if we find that a specific sector is unfilled, we will simply return nil
	local centerPoint = (self.Bounds.Min + self.Bounds.Max)/2
	if point.X <= centerPoint.X then
		--Top left, else bottom left
		if point.Y <= centerPoint.Y then
			return self.TopLeft and self.TopLeft:Locate(point)
		else
			return self.BottomLeft and self.BottomLeft:Locate(point)
		end
	else
		--Top right, else bottom right
		if point.Y <= centerPoint.Y then
			return self.TopRight and self.TopRight:Locate(point)
		else
			return self.BottomRight and self.BottomRight:Locate(point)
		end
	end
end

As for the driver code that’ll piece it all together, the results are best seen in an actual game. So, I put together a model that has the dummy map I made + two local scripts: one to test the binary tree and one to test the quad tree. The full source code will be there.
https://gyazo.com/b841af56176190e9244f9423eddac6b6.gif
This worked incredibly great on my computer, with on average a 0.03ms search time. Given that was based on 16 nodes, in theory if I divided the map into a million nodes the performance would still be around a mere 0.5ms per node (going back to my statement about BST efficiency earlier). lg(n) efficiency is every programmer’s dream, and now we can have that for grid collisions!
Trees.rbxm (6.4 KB)
I hope this tutorial helped you learn some ways to better handle things like grid collisions in a game. There’s so much more potential to be discovered in octrees, which I may do a tutorial on later. Thanks for reading :stuck_out_tongue:

41 Likes

Wow. This is really cool! Would a node system be better at providing general guidance system and area for a NPC to move about and ‘live’ in a space rather than setting up cframe/tween or pathfinding systems?

You could use a pathfinding algorithm to have control on how you move a character. The one I know best is the A* search method. Though I haven’t specifically used it I know it’s one of the better pathfinding algorithms that beginners tend to use. There’s also tons of deeper (and much more efficient) ways of approaching this. But like I said earlier this stuff doesn’t really have a great place in game development. I imagine setting up a node system would be much more difficult than using something like Roblox’s pathfinding service or just spitting out random points in a radius. So I wouldn’t say setting up a node grid would necessarily be better in your case. I do encourage you to try it out, if you have proficiency with handling grids you could definitely make something cool out of it.

Thank you for the lead on A*search. I’ll look into that.

The system of nodes was recently presented to me which is likely why you post code my eye. Well, actually it was because I was looking up information about modeling trees (literally trees with leaves and branches in the workspace) and read you post.

As far as my pursuit of crafting something myself with nodes - yeah, to say that I’m good at anything involving scripting would be a stretch but I’ll keep at it.

I think I must be misunderstanding your practical use examples in your original post. I see that you’ve stated again that you don’t think it’s really effective for game development but - would you consider saying again what the possible example would be for using this Binary or Quad Tree ?

The only time I’ve found a binary tree (not a BST, an unsorted binary tree) to be useful is a dialogue handler. For example you have an NPC that asks the player something, if they say no move to the left node, if they say yes move to the right node, and that child node continues the conversation.

Quad trees are primarily used for 2d collision. That’s the only use case I can possibly think of and if you search the internet that’ll be how its used for the most part. You shouldn’t use this kind of stuff if simple code offers a cleaner, more apparent solution. However custom collision handling does get mind boggling easily and quad trees are an excellent data structure to help with that.

1 Like

that’s very interesting. Thank you for holding my hand through your example again. I think that anything that’s able to sort large amounts of information is worth knowing and learning about. Thanks again!