Your main.lua is past a hundred lines and every activity from here adds more. This is the moment to split it up — before it hurts, and while the seams are still obvious.
Nothing new is coming. Activity 1.12 already taught the whole mechanism: a module is a table a file returns, and require runs that file once and hands you the table.
skynest/ main.lua conf.lua enemy.lua collision.lua ship.pnglocal Enemy = require("enemy")local collision = require("collision")No paths, no extensions, no build step. LÖVE looks in your game folder, so a file sitting next to main.lua is found by its name alone.
The test is not "how many lines" but how much a piece needs to know about everything else. Two things in your file barely know anything:
overlaps takes two tables and returns a boolean. It has never heard of bullets, enemies, or the player. That is a perfect module — it could be dropped into any other game unchanged.Enemy knows its own position, speed and hp, and nothing about the score or the player. It only needs love.graphics, which is global anyway.The update loop, by contrast, touches everything at once, and that is why it stays in main.lua. Do not split something just to make the file shorter — a module that needs six things passed into it has moved the mess rather than removed it.
collision.lua returns a table of plain functions — a toolbox. You call collision.overlaps(a, b) with a dot.
enemy.lua returns a class table — a thing you make instances from. You call Enemy.new(x) with a dot and e:update(dt) with a colon, exactly as before.
Both are just tables being returned. The difference is entirely in what you put in them, which is the point Activity 1.10 kept making.
local Enemy = require("enemy") — the file is lowercase, the variable is capitalized, and nothing enforces either. That is a convention worth following: lowercase filenames, capitalized class variables, so a reader can tell a class from a toolbox at the point of use.
Remember that require runs the file once. If two files both require enemy.lua, they get the same table — which is exactly right for a class, and something to watch for if a module ever holds mutable state of its own.
This activity is the first that adds no features. When you finish it the game will look and play identically, and that is the success condition. If behavior changed, something went wrong in the move.
So make one change at a time and run after each. Moving two things at once and finding the game broken leaves you guessing which move did it — and a refactor is the one situation where you have a perfect reference to compare against.
Run the game first and watch it for a few seconds. That is your reference. Everything below should leave it unchanged.
overlaps in it, and returns it.overlaps from main.lua and require the module at the top instead.collision.overlaps(...).-- collision.lua
local collision = {}
function collision.overlaps(a, b) return a.x - a.size / 2 < b.x + b.size / 2 and a.x + a.size / 2 > b.x - b.size / 2 and a.y - a.size / 2 < b.y + b.size / 2 and a.y + a.size / 2 > b.y - b.size / 2end
return collision-- main.lua
local collision = require("collision")
local shiplocal player = { x = 400, y = 500, size = 40, speed = 300 }local bullets = {}local fireCooldown = 0local enemies = {}local spawnTimer = 0local spawnEvery = 0.8local score = 0
local Enemy = {}Enemy.__index = Enemy
function Enemy.new(x) return setmetatable({ x = x, y = -20, size = 30, speed = 120, hp = 2 }, Enemy)end
function Enemy:update(dt) self.y = self.y + self.speed * dtend
function Enemy:draw() love.graphics.rectangle("fill", self.x - self.size / 2, self.y - self.size / 2, self.size, self.size)end
function love.load() love.graphics.setBackgroundColor(0.05, 0.05, 0.1) math.randomseed(os.time()) ship = love.graphics.newImage("ship.png")end
function love.update(dt) if love.keyboard.isDown("left", "a") then player.x = player.x - player.speed * dt end if love.keyboard.isDown("right", "d") then player.x = player.x + player.speed * dt end
local half = player.size / 2 player.x = math.max(half, math.min(love.graphics.getWidth() - half, player.x))
fireCooldown = fireCooldown - dt if love.keyboard.isDown("space") and fireCooldown <= 0 then table.insert(bullets, { x = player.x, y = player.y - 30, size = 6, speed = 500 }) fireCooldown = 0.15 end
for i = #bullets, 1, -1 do local b = bullets[i] b.y = b.y - b.speed * dt if b.y < -20 then table.remove(bullets, i) end end
spawnTimer = spawnTimer - dt if spawnTimer <= 0 then table.insert(enemies, Enemy.new(math.random(40, love.graphics.getWidth() - 40))) spawnTimer = spawnEvery end
for i = #enemies, 1, -1 do local e = enemies[i] e:update(dt) if e.y > love.graphics.getHeight() + 20 then table.remove(enemies, i) end end
for i = #enemies, 1, -1 do if collision.overlaps(enemies[i], player) then enemies = {} bullets = {} score = 0 break end end
for i = #enemies, 1, -1 do for j = #bullets, 1, -1 do if collision.overlaps(enemies[i], bullets[j]) then table.remove(bullets, j) enemies[i].hp = enemies[i].hp - 1 if enemies[i].hp <= 0 then table.remove(enemies, i) score = score + 1 end break end end endend
function love.draw() love.graphics.setColor(1, 1, 1) love.graphics.draw(ship, player.x, player.y, 0, 2, 2, ship:getWidth() / 2, ship:getHeight() / 2)
love.graphics.setColor(1, 0.9, 0.3) for _, b in ipairs(bullets) do love.graphics.rectangle("fill", b.x - 2, b.y - 8, 4, 16) end
love.graphics.setColor(1, 0.3, 0.4) for _, e in ipairs(enemies) do e:draw() end
love.graphics.setColor(1, 1, 1) love.graphics.print("Score: " .. score, 10, 10)endEnemy.new and e:update(dt) read exactly as they did, because the class was always just a table and now the table arrives from somewhere else.-- enemy.lua
local Enemy = {}Enemy.__index = Enemy
function Enemy.new(x) return setmetatable({ x = x, y = -20, size = 30, speed = 120, hp = 2 }, Enemy)end
function Enemy:update(dt) self.y = self.y + self.speed * dtend
function Enemy:draw() love.graphics.setColor(1, 0.3, 0.4) love.graphics.rectangle("fill", self.x - self.size / 2, self.y - self.size / 2, self.size, self.size)end
return Enemy-- main.lua
local Enemy = require("enemy")local collision = require("collision")
local shiplocal player = { x = 400, y = 500, size = 40, speed = 300 }local bullets = {}local fireCooldown = 0local enemies = {}local spawnTimer = 0local spawnEvery = 0.8local score = 0
function love.load() love.graphics.setBackgroundColor(0.05, 0.05, 0.1) math.randomseed(os.time()) ship = love.graphics.newImage("ship.png")end
function love.update(dt) if love.keyboard.isDown("left", "a") then player.x = player.x - player.speed * dt end if love.keyboard.isDown("right", "d") then player.x = player.x + player.speed * dt end
local half = player.size / 2 player.x = math.max(half, math.min(love.graphics.getWidth() - half, player.x))
fireCooldown = fireCooldown - dt if love.keyboard.isDown("space") and fireCooldown <= 0 then table.insert(bullets, { x = player.x, y = player.y - 30, size = 6, speed = 500 }) fireCooldown = 0.15 end
for i = #bullets, 1, -1 do local b = bullets[i] b.y = b.y - b.speed * dt if b.y < -20 then table.remove(bullets, i) end end
spawnTimer = spawnTimer - dt if spawnTimer <= 0 then table.insert(enemies, Enemy.new(math.random(40, love.graphics.getWidth() - 40))) spawnTimer = spawnEvery end
for i = #enemies, 1, -1 do local e = enemies[i] e:update(dt) if e.y > love.graphics.getHeight() + 20 then table.remove(enemies, i) end end
for i = #enemies, 1, -1 do if collision.overlaps(enemies[i], player) then enemies = {} bullets = {} score = 0 break end end
for i = #enemies, 1, -1 do for j = #bullets, 1, -1 do if collision.overlaps(enemies[i], bullets[j]) then table.remove(bullets, j) enemies[i].hp = enemies[i].hp - 1 if enemies[i].hp <= 0 then table.remove(enemies, i) score = score + 1 end break end end endend
function love.draw() love.graphics.setColor(1, 1, 1) love.graphics.draw(ship, player.x, player.y, 0, 2, 2, ship:getWidth() / 2, ship:getHeight() / 2)
love.graphics.setColor(1, 0.9, 0.3) for _, b in ipairs(bullets) do love.graphics.rectangle("fill", b.x - 2, b.y - 8, 4, 16) end
love.graphics.setColor(1, 0.3, 0.4) for _, e in ipairs(enemies) do e:draw() end
love.graphics.setColor(1, 1, 1) love.graphics.print("Score: " .. score, 10, 10)endsetColor(1, 0.3, 0.4) out of love.draw and into Enemy:draw.setColor(1, 1, 1) more than ever.-- main.lua
local Enemy = require("enemy")local collision = require("collision")
local shiplocal player = { x = 400, y = 500, size = 40, speed = 300 }local bullets = {}local fireCooldown = 0local enemies = {}local spawnTimer = 0local spawnEvery = 0.8local score = 0
function love.load() love.graphics.setBackgroundColor(0.05, 0.05, 0.1) math.randomseed(os.time()) ship = love.graphics.newImage("ship.png")end
function love.update(dt) if love.keyboard.isDown("left", "a") then player.x = player.x - player.speed * dt end if love.keyboard.isDown("right", "d") then player.x = player.x + player.speed * dt end
local half = player.size / 2 player.x = math.max(half, math.min(love.graphics.getWidth() - half, player.x))
fireCooldown = fireCooldown - dt if love.keyboard.isDown("space") and fireCooldown <= 0 then table.insert(bullets, { x = player.x, y = player.y - 30, size = 6, speed = 500 }) fireCooldown = 0.15 end
for i = #bullets, 1, -1 do local b = bullets[i] b.y = b.y - b.speed * dt if b.y < -20 then table.remove(bullets, i) end end
spawnTimer = spawnTimer - dt if spawnTimer <= 0 then table.insert(enemies, Enemy.new(math.random(40, love.graphics.getWidth() - 40))) spawnTimer = spawnEvery end
for i = #enemies, 1, -1 do local e = enemies[i] e:update(dt) if e.y > love.graphics.getHeight() + 20 then table.remove(enemies, i) end end
for i = #enemies, 1, -1 do if collision.overlaps(enemies[i], player) then enemies = {} bullets = {} score = 0 break end end
for i = #enemies, 1, -1 do for j = #bullets, 1, -1 do if collision.overlaps(enemies[i], bullets[j]) then table.remove(bullets, j) enemies[i].hp = enemies[i].hp - 1 if enemies[i].hp <= 0 then table.remove(enemies, i) score = score + 1 end break end end endend
function love.draw() love.graphics.setColor(1, 1, 1) love.graphics.draw(ship, player.x, player.y, 0, 2, 2, ship:getWidth() / 2, ship:getHeight() / 2)
love.graphics.setColor(1, 0.9, 0.3) for _, b in ipairs(bullets) do love.graphics.rectangle("fill", b.x - 2, b.y - 8, 4, 16) end
for _, e in ipairs(enemies) do e:draw() end
love.graphics.setColor(1, 1, 1) love.graphics.print("Score: " .. score, 10, 10)endbullet.new(x, y) factory function — both are defensible and the reasoning matters more than the answer.collision.circles(a, b) alongside overlaps and switch between them by changing one word at the call sites. That is the payoff of a toolbox module.local enemies = require('enemy') and use enemies.Fast.new(x).The game plays exactly as it did before. Same speed, same spawn rate, same two hits per enemy, same reset. Nothing about it should feel different.
What has changed is the shape of the project:
If you get an error naming a nil value, a call site still uses the old bare name. If enemies vanish, check that enemy.lua ends with return Enemy — a module that returns nothing gives you a nil with no complaint at the point of require.