Example: Each autotile is a set of six base tiles that are cut up and reformed into result tiles. This is what I call each base tile: Island – The island base tile is what a result tile looks like if it is not connected to any surrounding tiles. This is the only tile that is not cut apart.
Cross – This is the base tile in the top-right of the image that looks like a +. This is what a result tile looks like if it is connected to the tiles above, below, left, and right of it.
Corners – These are the remaining four base tiles. They represent what a result tile looks like if it is the “corner” piece.
The basic idea is that you can make tiles look correct in any context simply by cutting apart these base tiles into 4 quarter pieces that I call chunks, and then stitching different chunks together to form a result tile that looks natural and connected to other result tiles. As a whole, the autotile needs to know about it's 8 surrounding adjacent tiles. However, chunks only need to know about the three tiles they are touching. Each of these tiles can be labeled as left, right, or center depending on the placement from the chunk.
Notice how an adjacent tile can be labeled differently depending on which chunk you are looking at. For example, the top adjacent tile is to the “right” of the top-left chunk but to the “left” of the top-right chunk.
All right. It’s important that you understand everything up to this point before we continue.
- Autotile sets consist of 6 base tiles which are Island, Cross, and 4 Corners.
- Each tile is separated into 4 pieces called chunks, except for the island tile.
- Each chunk needs to know about the three adjacent tiles it’s touching. These tiles are labeled left, right, and center.
- Depending on the tile’s surroundings, chunks are reformed into a result tile.
The first thing we will do is cut out the whole island tile and then the rest of the tiles we will cut apart into chunks. We will be using quads to do this. We will separate chunks into 4 arrays, one for each quadrant the chunks are cut from. We will call the arrays TL (TopLeft), TR (TopRight), BL (BottomLeft), and BR (BottomRight).
How we index each chunk inside the array is important. We will index them by what I call their adjacent value. The adjacent value is a number that represents which adjacent tiles the chunk is connected to. The value ranges from 0 to 4.
- 0 - No connected tiles
- 1 - Left tile is connected
- 2 - Right tile is connected
- 3 - Left and Right tiles are connected
- 4 - Left, Right, and Center tiles are connected.
Here is a chart that shows what each chunk’s value is and what array it belongs to. Here some example code that cuts out the chunks and indexes them:
Code: Select all
local autotileImage = love.graphics.newImage("Autotile.png") -- Autotile image
local width = autotileImage:getWidth() -- Image width
local height = autotileImage:getHeight() -- Image height
local size = 32 -- Tile size
local hsize = math.floor(size/2) -- Half tile size
-- Chunk arrays
local chunk = {TL={}, TR={}, BL={}, BR={}}
-- Island tile
chunk.island = love.graphics.newQuad(0, 0, size, size, width, height)
-- This cuts a tile into chunks
local function cutTile(x,y)
local TL = love.graphics.newQuad(x, y, hsize, hsize, width, height)
local TR = love.graphics.newQuad(x+hsize, y, hsize, hsize, width, height)
local BL = love.graphics.newQuad(x, y+hsize, hsize, hsize, width, height)
local BR = love.graphics.newQuad(x+hsize, y+hsize, hsize, hsize, width, height)
return TL,TR,BL,BR
end
-- Cut out the chunks and index them by their adjacent tile value
chunk.TL[3], chunk.TR[3], chunk.BL[3], chunk.BR[3] = cutTile(size,0)
chunk.TL[0], chunk.TR[2], chunk.BL[1], chunk.BR[4] = cutTile(0,size)
chunk.TL[1], chunk.TR[0], chunk.BL[4], chunk.BR[2] = cutTile(size, size)
chunk.TL[2], chunk.TR[4], chunk.BL[0], chunk.BR[1] = cutTile(0, size*2)
chunk.TL[4], chunk.TR[1], chunk.BL[2], chunk.BR[0] = cutTile(size, size*2)
Code: Select all
-- A 2-d grid used to represent connected tiles. Values can either contain true for
-- connected tiles or nil for unconnected tiles.
local grid = {}
-- This function calculates the adjacent tile values and draws the autotile
function drawAutotile(x,y)
-- Calculate the adjacent tile values for each chunk
local val = {TL=0, TR=0, BL=0, BR=0}
if grid[x] and grid[x][y-1] then val.TL = val.TL + 2; val.TR = val.TR + 1 end -- top
if grid[x] and grid[x][y+1] then val.BL = val.BL + 1; val.BR = val.BR + 2 end -- bottom
if grid[x-1] and grid[x-1][y] then val.TL = val.TL + 1; val.BL = val.BL + 2 end -- left
if grid[x+1] and grid[x+1][y] then val.TR = val.TR + 2; val.BR = val.BR + 1 end -- right
if grid[x-1] and grid[x-1][y-1] and val.TL == 3 then val.TL = 4 end -- topleft
if grid[x+1] and grid[x+1][y-1] and val.TR == 3 then val.TR = 4 end -- topright
if grid[x-1] and grid[x-1][y+1] and val.BL == 3 then val.BL = 4 end -- bottomleft
if grid[x+1] and grid[x+1][y+1] and val.BR == 3 then val.BR = 4 end -- bottomright
-- If isolated then draw the island.
if val.TL == 0 and val.TR == 0 and val.BL == 0 and val.BR == 0 then
love.graphics.draw(autotileImage, chunk.island, x*size,y*size)
-- Otherwise, draw the chunks
else
love.graphics.draw(autotileImage, chunk.TL[val.TL], x*size,y*size)
love.graphics.draw(autotileImage, chunk.TR[val.TR], x*size+hsize,y*size)
love.graphics.draw(autotileImage, chunk.BL[val.BL], x*size,y*size+hsize)
love.graphics.draw(autotileImage, chunk.BR[val.BR], x*size+hsize,y*size+hsize)
end
end
Now it's a very simple matter to draw the tiles. We'll just finish up by defining a couple of love functions. To test out the tiles let's have the left mouse button place down an autotile and the right mouse button remove it.
Code: Select all
function love.update()
-- If a tile is leftclicked then make it an autotile
if love.mouse.isDown('l') then
local x,y = math.floor(love.mouse.getX()/size), math.floor(love.mouse.getY()/size)
if not grid[x] then grid[x] = {} end
grid[x][y] = true
end
-- If a tile is rightclicked then erase the autotile
if love.mouse.isDown('r') then
local x,y = math.floor(love.mouse.getX()/size), math.floor(love.mouse.getY()/size)
if grid[x] then grid[x][y] = nil end
end
end
function love.draw()
-- Draw the autotiles
local endx = math.ceil(love.graphics.getWidth() /size)
for x = 0, math.ceil(love.graphics.getWidth() /size) do
for y = 0, math.ceil(love.graphics.getHeight() /size) do
if grid[x] and grid[x][y] then
drawAutotile(x,y)
else
love.graphics.draw(floorImage, x*size, y*size)
end
end
end
--Draw the cursor
local mx, my = love.mouse.getPosition()
love.graphics.setColor(0,0,0,255)
love.graphics.rectangle("line", mx+1 - mx % size, my+1 - my % size, size, size)
love.graphics.setColor(255,255,255,255)
love.graphics.rectangle("line", mx - mx % size, my - my % size, size, size)
-- Instructions
love.graphics.setColor(0,0,0,100)
love.graphics.rectangle('fill',0,0,350,20)
love.graphics.setColor(255,255,255,255)
love.graphics.print("Left click to place tiles. Right click to delete them",5,5)
end
A couple of other things: Drawing the autotiles this way without the aid of sprite batches or canvases is likely very slow. You would probably only want to find the adjacent tile value when the autotile or adjacent tiles change. The other thing is that chunks do not necessarily have to be cut into 4 equal parts. For some types of tiles you might want to cut them off center a bit (such as perspective walls).