Procedural Tiling w/ Applications in Chess, Plates, & Voxel Terrain Generation

Hello, I’m Xitral!

I’ve been researching and experimenting with procedural tile mapping in Lua for a couple of months now and wanted to share what I’ve learned.

This tutorial will show you how to make multiple types of procedural tiles ranging from hexagons to chess boards.

This tutorial will start off with the basics and will eventually get more intricate.




Example of hexgonal tiling, HEX Test Build v0.1.

Tiling, a.k.a tessellation, is a mathematical term meaning:

“a way of arranging identical plane shapes so that they completely cover an area without overlapping.” ~ Oxford Languages

From this definition, we know there are two basic rules in tiling:

  1. No overlapping!
  2. No gaps!

With the definition of tiling in mind, we can easily define Procedural Tiling as the procedure of tiling infinitely (or finitely).



Tutorial 1: Your First Procedural Tile Map (For Beginners)

Now that we’ve got the boring definition out of the way, let’s do some coding! :sunglasses:

In this first program we are going to be making something similar to this simple part grid as shown below:

Step 1: Getting your workspace ready

  • Step 1a: The first step in scripting a procedural tilemap is of course adding a new ServerScript. (This script can be anywhere, but I recommend using ServerScriptService)
  • Step 1b: Now go grab a snack, reward yourself for adding a script into the game! :sunglasses:

Step 2: Scripting Prerequisites (a.k.a. cleaning up before the mess)

Adding about 100+ parts to the Workspace in under 10 seconds is quite messy, and sometimes costly. So let’s fix that, before it comes back to bite us in the future.

local folder = Instance.new("Folder", workspace) 
-- Instance.new() has two arguments, the object class and the parent. 

Nice! Now we just need a file cabinet to keep this office spick and span.

local folder = Instance.new("Folder", workspace) 
-- Instance.new() has two arguments, the object class and the parent. 

local origin = Vector3.new(0,0,0) -- Sets a point of origin (Can be any position).
local numRows = 5 -- Number of rows.
local numCols = 5 -- Number of columns.

Alright, we got some variables! I set the origin to (0,0,0) for simplicity, but any position will work.
Note: The total number of tiles you will generate can be found using this equation:

Number of Rows * Number of Columns = Total Tiles


Step 3: The Meat of the Code

For procedural tiling, we need to tell the script to iterate (loop) through a set number of dimensions. For this program, we only have two dimensions: X and Z

X, Y, Z are the three axes of the 3-D plane

  • Step 3a: Create two nested for loops, one for the X-axis, and one for the Z-axis, the order doesn’t matter much.
local folder = Instance.new("Folder", workspace) 
-- Instance.new() has two arguments, the object class and the parent. 

local origin = Vector3.new(0,0,0) -- Sets a point of origin (Can be any position).
local numRows = 5 -- Number of rows.
local numCols = 5 -- Number of columns.

for x = 1, numRows do
	for z = 1, numCols do
		
	end
end
  • Step 3b: Create a new part nested inside both loops! Make sure the parent of that part is the folder we made earlier.
local folder = Instance.new("Folder", workspace) 
-- Instance.new() has two arguments, the object class and the parent. 

local origin = Vector3.new(0,0,0) -- Sets a point of origin (Can be any position).
local numRows = 5 -- Number of rows.
local numCols = 5 -- Number of columns.

for x = 1, numRows do
	for z = 1, numCols do
		local part = Instance.new("Part", folder)
		part.Size = Vector3.new(4,1,4)
		part.TopSurface = Enum.SurfaceType.Smooth
		part.BottomSurface = Enum.SurfaceType.Smooth
		part.Anchored = true
	end
end

Important note: Make sure the part is anchored. Also, make sure it’s a square (rectangles can’t tile!) by making the X and Z of the size equal. Last bit, if you don’t want the stud appearance you will need to change the TopSurface and the BottomSurface

  • Step 3c: Now for the most important part, setting the position of the part so that it tiles. For this, we will need to do some simple math.

For each axis (X and Z) we can find the position of the part by this equation:

Axis Position = Axis Value of Origin + (Axis Value of Part Size * Current Index)

With that equation, we can create a Vector3 Value (a.k.a. position):

		Vector3.new(
			origin.X + (part.Size.X * x), -- X
			origin.Y, -- Y
			origin.Z + (part.Size.Z * z) -- Z
		)

Making the full script:

local folder = Instance.new("Folder", workspace) 
-- Instance.new() has two arguments, the object class and the parent. 

local origin = Vector3.new(0,0,0) -- Sets a point of origin (Can be any position).
local numRows = 5 -- Number of rows.
local numCols = 5 -- Number of columns.

for x = 1, numRows do
	for z = 1, numCols do
		local part = Instance.new("Part", folder)
		part.Size = Vector3.new(4,1,4)
		part.TopSurface = Enum.SurfaceType.Smooth
		part.BottomSurface = Enum.SurfaceType.Smooth
		part.Anchored = true
		
		part.Position = Vector3.new(
			origin.X + (part.Size.X * x),
			origin.Y,
			origin.Z + (part.Size.Z * z)
		)
	end
end

We have finished the program! but wait…

Step 4 (Optional): Offsets

If you’ve ever played Lab Experiment or Plates of Fate you’ll immediately recognize this next design.

It’s easy: In order to add offset we just need to multiply

New Position = Axis Position * Axis Offset

		Vector3.new(
			origin.X + (part.Size.X * x * 2), -- Notice the new * 2
			origin.Y,
			origin.Z + (part.Size.Z * z * 2) -- Notice the new * 2
		)

Behold! Offset!

Tutorial 2: Procedural Chessboard (ETC TBD)

24 Likes

woah, this is a nice tutorial.

Nice to see more tutorials on the topic of proceduralism. Might use this in some randomized board game.

Hello, please do not use the second argument of Instance.new - even Roblox engineers heavily advice against it.

2 Likes

Thanks for the optimization tip! :slightly_smiling_face:
Never knew parenting syntax could cause performance issues. I’ll be sure to use the optimized syntax in the updated tutorials.