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:
- No overlapping!
- 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!
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!
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.
- Step 2a: Create a folder from the script using Instance.new().
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.
- Step 2b: Add some variables!
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
)