Bullets were plain tables, and that was fine because a bullet does almost nothing. Enemies will grow behavior — they move, they take damage, later they die in interesting ways — so they are worth making properly.
This is Activity 1.11, unchanged.
local Enemy = {}Enemy.__index = Enemy
function Enemy.new(x) return setmetatable({ x = x, y = -20, size = 30, speed = 120 }, 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)endFour lines you already know: an empty table for the class, Enemy.__index = Enemy to make lookups fall back to it, a constructor that attaches it with setmetatable, and methods defined with a colon so they get self.
What it buys you is the shared-method property. Fifty enemies hold fifty positions but there is still exactly one update function. And your main loop gets shorter: it says e:update(dt) and stops caring how an enemy moves.
Notice where the knowledge now lives. The main file no longer knows an enemy's speed or shape. Change Enemy:draw to render a sprite instead of a square and not one line of love.draw changes. That is the whole argument for the pattern, and it is what makes Activity 2.8 possible.
The same accumulator you used for the weapon, with a different number.
spawnTimer = spawnTimer - dt
if spawnTimer <= 0 then table.insert(enemies, Enemy.new(math.random(40, love.graphics.getWidth() - 40))) spawnTimer = spawnEveryendStarting spawnTimer at 0 means the first enemy arrives immediately rather than after a wait. Start it at spawnEvery instead and the player gets a moment of calm first. Both are reasonable; pick deliberately.
The spawn x is random but inset from the edges — math.random(40, width - 40) keeps an enemy from being born half off the screen. And enemies start at a negative y so they slide into view rather than popping into existence at the top edge.
Seed the generator in love.load. This is the warning from Activity 1.4 coming due: LÖVE's Lua does not seed itself, so without math.randomseed(os.time()) every playthrough gets the identical wave pattern. It will look deliberate and it is not.
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) endendIdentical in shape to the bullet loop, and backwards for the same reason. Enemies that reach the bottom are removed, and in a finished game that would also cost the player something — Activity 2.9 adds lives.
An enemy leaving the bottom and a bullet leaving the top are the same problem. If you find yourself writing this loop a third time, that is the signal to write a helper that takes a list and a test. Resist it until then; two is not yet a pattern.
Same file. You are giving the player something to shoot at.
new, update and draw methods.love.load so you have something on screen before the timer exists.-- main.lua
local shiplocal player = { x = 400, y = 500, size = 40, speed = 300 }local bullets = {}local fireCooldown = 0local enemies = {}
local Enemy = {}Enemy.__index = Enemy
function Enemy.new(x) return setmetatable({ x = x, y = -20, size = 30, speed = 120 }, 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") table.insert(enemies, Enemy.new(200)) table.insert(enemies, Enemy.new(600))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
for _, e in ipairs(enemies) do e:update(dt) 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("Enemies: " .. #enemies, 10, 10)endlove.load.-- main.lua
local shiplocal player = { x = 400, y = 500, size = 40, speed = 300 }local bullets = {}local fireCooldown = 0local enemies = {}local spawnTimer = 0local spawnEvery = 0.8
local Enemy = {}Enemy.__index = Enemy
function Enemy.new(x) return setmetatable({ x = x, y = -20, size = 30, speed = 120 }, 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 _, e in ipairs(enemies) do e:update(dt) 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("Enemies: " .. #enemies, 10, 10)endspawnEvery to 0.2 and watch that steady number roughly quadruple. One variable controls the difficulty of the whole game.-- main.lua
local shiplocal player = { x = 400, y = 500, size = 40, speed = 300 }local bullets = {}local fireCooldown = 0local enemies = {}local spawnTimer = 0local spawnEvery = 0.8
local Enemy = {}Enemy.__index = Enemy
function Enemy.new(x) return setmetatable({ x = x, y = -20, size = 30, speed = 120 }, 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 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("Enemies: " .. #enemies, 10, 10)endspawnEvery slightly on every spawn, with a math.max floor so it never reaches zero.math.sin of an accumulated time.
Enemies arriving on a timer at random x, falling while the bullets climb past them. Nothing collides yet.
Bullets ignoring enemies is correct for now — nothing has been told to check. That is Activity 2.7, and it is about twelve lines.