Loading Grid Based Level Files with Love2d

Series: dev April 17, 2018

In my current project, I’m building tile-based levels and wanted a way to quickly build and edit them. I opted for a simple text based system. Each character is a different entity that will be loaded into the level. Each tile has a fixed width and height that I’ve defined in game.tile.width and game.tile.height In the example below, the player is X, wall tiles are 1, and lava tiles are 2. H/h, D/d, and W/w are powerups the character will pickup.

1       111                                              D             111     1111111111
1   X   111                                                              1     1111111111
1       111                                               d              1     1111111111
1       111                                111    11     11        11    1111  1111111111
1       111                     1111111                                     1  1111111111
1       111                                                                 1  1111111111
1                            111111111111                                   1  1111111111
1                         1                 1    H                          1       W   1
1    *     1      1      111               11     h                         1        w  1
11111111111111211111111111111111122111111111111111111111111111111111111111111111111111111

First, to load the file, we just use love.filesystem.lines, which allows us to iterate over a series of lines. We also set the starting y position to 0.

function build(filename)
	local y=0
	local lines = love.filesystem.lines(filename)
	for line in love.filesystem.lines(filename) do
		-- This is where we deal with each line individually.
	end
end

I tend to store the level files in a levels directory. In my game, I calling the build function with build('levels/lvl1.lvl').

Next, we need to iterate over each character in the line. To do this, we just create a for loop between 1 and the length of our line using #line. At the end of the for loop, once we’ve completed our work on the line, we’ll increment the y position by 1. I’m mentioning this now, but remember, first we process each character on a line, and then we bump the y value up by one.

		for x = 1, #line do
			-- Here, we'll process each character individually.
		end
		y = y + 1

Finally, we process each line individually. We get the character at position x on our line using line:sub(x,). Then we just use elesif statements to determine what the character we’re looking at is, and add the correct entity into the world at the correct location.

			local c = line:sub(x,x)
			if c == 'X' then 
				tiny.addEntity(world, entity.player(t.x*x,t.y*y))
				game.start_position = component.position(t.x*x,t.y*y,1,1)
			elseif c == 'S' then 
				tiny.addEntity(world, entity.superplayer(t.x*x,t.y*y))
			elseif c == 'i' then
				tiny.addEntity(world, entity.world_text("Press i for inputs",t.x*x,t.y*y,{255,255,255},false))
			elseif c == 'c' then
				tiny.addEntity(world, entity.world_text("c for highjump",t.x*x,t.y*y,{255,255,255},false))
			elseif c == '1' then tiny.addEntity(world, entity.block(t.x*x,t.y*y))
			elseif c == '2' then tiny.addEntity(world, entity.lava(t.x*x,t.y*y))
			elseif c == '6' then tiny.addEntity(world, entity.wall_eye(t.x*x,t.y*y, -1))
			elseif c == '7' then tiny.addEntity(world, entity.wall_eye(t.x*x,t.y*y, 1))
			elseif c == '8' then tiny.addEntity(world, entity.ladder(t.x*x,t.y*y))
			elseif c == '*' then tiny.addEntity(world, entity.bonfire(t.x*x,t.y*y,false))
			-- Pickups
			elseif c == 'd' then tiny.addEntity(world, entity.pickup(t.x*x,t.y*y,{double_jump = component.double_jump()}))
			elseif c == 'h' then tiny.addEntity(world, entity.pickup(t.x*x,t.y*y,{high_jump = true}))
			elseif c == 'w' then tiny.addEntity(world, entity.pickup(t.x*x,t.y*y,{wall_jump = true}))
			elseif c == 'b' then tiny.addEntity(world, entity.pickup(t.x*x,t.y*y,{bomber = true}))
			elseif c == 'D' then tiny.addEntity(world, entity.world_text("Double JumP",t.x*x,t.y*y,{255,255,255},false))
			elseif c == 'H' then tiny.addEntity(world, entity.world_text("High JumP (C/Square)",t.x*x,t.y*y,{255,255,255},false))
			elseif c == 'W' then tiny.addEntity(world, entity.world_text("Wall JumP",t.x*x,t.y*y,{255,255,255},false))
			elseif c == 'B' then tiny.addEntity(world, entity.world_text("Boom! (X/Circle)",t.x*x,t.y*y,{255,255,255},false))
			elseif c == 'E' then tiny.addEntity(world, entity.world_text("The end?",t.x*x,t.y*y,{255,255,255},false))
			elseif c == 'l' then tiny.addEntity(world, entity.world_text("...Left",t.x*x,t.y*y,{255,255,255},false))
			elseif c == 'k' then tiny.addEntity(world, entity.world_text("KeeP going (this area's boring...)",t.x*x,t.y*y,{255,255,255},false))
			end

When we put it all together, you get something like this:

function build(filename)
	local world = game.world
	world:clearEntities()
	local t = game.tile
	game.level_file_modified = love.filesystem.getLastModified(filename)
	local y=0
	local lines = love.filesystem.lines(filename)
	for line in love.filesystem.lines(filename) do
		for x = 1, #line do
		    local c = line:sub(x,x)
			if c == 'X' then 
				tiny.addEntity(world, entity.player(t.x*x,t.y*y))
				game.start_position = component.position(t.x*x,t.y*y,1,1)
			elseif c == 'S' then 
				tiny.addEntity(world, entity.superplayer(t.x*x,t.y*y))
			elseif c == 'i' then
				tiny.addEntity(world, entity.world_text("Press i for inputs",t.x*x,t.y*y,{255,255,255},false))
			elseif c == 'c' then
				tiny.addEntity(world, entity.world_text("c for highjump",t.x*x,t.y*y,{255,255,255},false))
			elseif c == '1' then tiny.addEntity(world, entity.block(t.x*x,t.y*y))
			elseif c == '2' then tiny.addEntity(world, entity.lava(t.x*x,t.y*y))
			elseif c == '6' then tiny.addEntity(world, entity.wall_eye(t.x*x,t.y*y, -1))
			elseif c == '7' then tiny.addEntity(world, entity.wall_eye(t.x*x,t.y*y, 1))
			elseif c == '8' then tiny.addEntity(world, entity.ladder(t.x*x,t.y*y))
			elseif c == '*' then tiny.addEntity(world, entity.bonfire(t.x*x,t.y*y,false))
			-- Pickups
			elseif c == 'd' then tiny.addEntity(world, entity.pickup(t.x*x,t.y*y,{double_jump = component.double_jump()}))
			elseif c == 'h' then tiny.addEntity(world, entity.pickup(t.x*x,t.y*y,{high_jump = true}))
			elseif c == 'w' then tiny.addEntity(world, entity.pickup(t.x*x,t.y*y,{wall_jump = true}))
			elseif c == 'b' then tiny.addEntity(world, entity.pickup(t.x*x,t.y*y,{bomber = true}))
			elseif c == 'D' then tiny.addEntity(world, entity.world_text("Double JumP",t.x*x,t.y*y,{255,255,255},false))
			elseif c == 'H' then tiny.addEntity(world, entity.world_text("High JumP (C/Square)",t.x*x,t.y*y,{255,255,255},false))
			elseif c == 'W' then tiny.addEntity(world, entity.world_text("Wall JumP",t.x*x,t.y*y,{255,255,255},false))
			elseif c == 'B' then tiny.addEntity(world, entity.world_text("Boom! (X/Circle)",t.x*x,t.y*y,{255,255,255},false))
			elseif c == 'E' then tiny.addEntity(world, entity.world_text("The end?",t.x*x,t.y*y,{255,255,255},false))
			elseif c == 'l' then tiny.addEntity(world, entity.world_text("...Left",t.x*x,t.y*y,{255,255,255},false))
			elseif c == 'k' then tiny.addEntity(world, entity.world_text("KeeP going (this area's boring...)",t.x*x,t.y*y,{255,255,255},false))
			end
		end
		y = y + 1
		print("Loading "..y)
	end
	tiny.addEntity(world, entity.overlay_text("Fall Start",game.view.width-45,10,{193,108,91},false))
end

As a bonus, since we’re storing the last time the level was modified, we can do something like this to auto-update the level whenever the level file is modified:

	if game.level_file_modified < love.filesystem.getLastModified(level_file) then
		build_level(level_file)
	end

built with , Jekyll, and GitHub Pages — read the fine print